-- HealerProtection v1.1 -- Turtle WoW / Vanilla 1.12.1 -- -- Goals of this version: -- * Native Vanilla-safe event handling (Lua 5.0 / 1.12 style globals). -- * No hard dependency on ClassicAPI, SuperWoW or Nampower. -- * Event-driven mana / health / death checks. -- * Lightweight polling only for aggro, because Vanilla has no reliable threat event. -- * Restored Near Death alerts. -- * Centralized chat/channel validation. -- * Removed dead bootstrap, translation and combat-lockdown code. HealerProtection = HealerProtection or {} local HP = HealerProtection local DB = nil local state = { oom = false, nearoom = false, neardeath = false, dead = false, aggro = false, channeling = false, } local warningAggro = nil local lastLosTarget = "" local nextLosMessage = 0 local DEFAULTS = { printnothing = false, showinraids = true, showoutsideofinstance = false, showinbgs = false, AGGRO = false, showaggrochat = true, showaggroemote = true, deathmessage = true, OOM = true, showoomchat = true, showoomemote = true, OOMPercentage = 10, NEAROOM = true, shownearoomchat = true, shownearoomemote = true, NEAROOMPercentage = 30, NEARDEATH = true, showneardeathchat = true, showneardeathemote = true, NEARDEATHPercentage = 30, notinsight = false, channelchat = "AUTO", prefix = "[Healer Protection]", suffix = "", } local TEXT = { aggro_warning = "You have aggro!", aggro_chat = "Aggro on me! Get it off!", oom = "I am OOM!", low_mana = "Low Mana!", low_health = "Help, I'm dying!", dead = "Healer is down... Fly, you fools!", los = "Target is not in line of sight", } -- ============================================================================ -- SMALL HELPERS -- ============================================================================ local function Clamp(value, low, high) value = tonumber(value) or low if value < low then return low end if value > high then return high end return value end local function GetRaidCount() if GetNumRaidMembers then return GetNumRaidMembers() or 0 end return 0 end local function GetPartyCount() if GetNumPartyMembers then return GetNumPartyMembers() or 0 end return 0 end local function IsInBattleground() if not GetBattlefieldStatus then return false end local maxQueues = MAX_BATTLEFIELD_QUEUES or 3 local i for i = 1, maxQueues do local status = GetBattlefieldStatus(i) if status == "active" then return true end end return false end local function IsInsideInstance() if not IsInInstance then -- Turtle WoW normally exposes IsInInstance(). If another 1.12 client -- does not, fail open rather than silently disabling the addon. return true end local inside = IsInInstance() return inside and true or false end local function GetMana() if UnitPower and UnitPowerMax then return UnitPower("player") or 0, UnitPowerMax("player") or 0 end return UnitMana("player") or 0, UnitManaMax("player") or 0 end local function PlayerUsesMana() if UnitPowerType then local _, token = UnitPowerType("player") return token == "MANA" end if UnitManaType then return UnitManaType("player") == 0 end return true end local function Percent(value, maximum) if not maximum or maximum <= 0 then return 0 end return math.floor((value * 1000 / maximum) + 0.5) / 10 end local function IsChannelAllowed(channel) if channel == "SAY" or channel == "YELL" then return true end if channel == "GUILD" then return GetGuildInfo and GetGuildInfo("player") ~= nil end if channel == "RAID" then return GetRaidCount() > 0 end if channel == "PARTY" then return GetPartyCount() > 0 or GetRaidCount() > 0 end return false end function HP:GetCurrentChannel() local channel = DB.channelchat or "AUTO" if channel ~= "AUTO" then return channel end if GetRaidCount() > 0 then return "RAID" end if GetPartyCount() > 0 then return "PARTY" end -- AUTO deliberately resolves to PARTY while solo. The centralized -- validation below prevents invalid SendChatMessage calls. return "PARTY" end function HP:CanAnnounce(channel) if DB.printnothing then return false end if not IsChannelAllowed(channel) then return false end if not DB.showoutsideofinstance and not IsInsideInstance() then return false end if not DB.showinbgs and IsInBattleground() then return false end if not DB.showinraids and GetRaidCount() > 0 then return false end return true end local function CleanAffix(text, before) if not text or text == "" or text == " " then return "" end if before then return text .. " " end return " " .. text end function HP:Announce(message) if not message or message == "" then return false end local channel = self:GetCurrentChannel() if not self:CanAnnounce(channel) then return false end local prefix = CleanAffix(DB.prefix, true) local suffix = CleanAffix(DB.suffix, false) SendChatMessage(prefix .. message .. suffix, channel) return true end local function DoSafeEmote(emote) if not state.channeling and DoEmote then DoEmote(emote) end end -- ============================================================================ -- ALERT LOGIC -- ============================================================================ function HP:CheckMana() if not PlayerUsesMana() then state.oom = false state.nearoom = false return end local mana, maxMana = GetMana() if maxMana <= 0 then return end local pct = Percent(mana, maxMana) local oomAt = Clamp(DB.OOMPercentage, 1, 99) local nearAt = Clamp(DB.NEAROOMPercentage, oomAt + 1, 99) -- OOM has priority. Mark near-OOM as already crossed so recovering from -- OOM does not immediately spam a second "Low Mana" warning. if DB.OOM and pct <= oomAt then if not state.oom then state.oom = true state.nearoom = true if DB.showoomchat then self:Announce(TEXT.oom .. " (" .. tostring(pct) .. "% Mana)") end if DB.showoomemote and self:CanAnnounce(self:GetCurrentChannel()) then DoSafeEmote("oom") end end elseif state.oom and pct > oomAt + 10 then state.oom = false end if DB.NEAROOM and not state.oom and pct <= nearAt and pct > oomAt then if not state.nearoom then state.nearoom = true if DB.shownearoomchat then self:Announce(TEXT.low_mana .. " (" .. tostring(pct) .. "% Mana)") end if DB.shownearoomemote and self:CanAnnounce(self:GetCurrentChannel()) then DoSafeEmote("incoming") end end elseif state.nearoom and pct > nearAt + 10 then state.nearoom = false end end function HP:CheckHealth() if UnitIsDead("player") then return end local health = UnitHealth("player") or 0 local maxHealth = UnitHealthMax("player") or 0 if maxHealth <= 0 then return end local pct = Percent(health, maxHealth) local threshold = Clamp(DB.NEARDEATHPercentage, 1, 99) if DB.NEARDEATH and pct <= threshold then if not state.neardeath then state.neardeath = true if DB.showneardeathchat then self:Announce(TEXT.low_health .. " (" .. tostring(pct) .. "% HP)") end if DB.showneardeathemote and self:CanAnnounce(self:GetCurrentChannel()) then DoSafeEmote("helpme") end end elseif state.neardeath and pct > threshold + 15 then state.neardeath = false end end function HP:HandleDeath() if state.dead then return end state.dead = true state.neardeath = false state.aggro = false if warningAggro then warningAggro:Hide() end if DB.deathmessage then self:Announce(TEXT.dead) end end function HP:HandleAlive() state.dead = false self:CheckHealth() self:CheckMana() end -- ============================================================================ -- AGGRO -- ============================================================================ local function UnitTargetsPlayer(unit) if not UnitExists(unit) then return false end if UnitIsFriend("player", unit) then return false end local targetOfTarget = unit .. "target" return UnitExists(targetOfTarget) and UnitIsUnit(targetOfTarget, "player") end local function GetAggroState() -- Use a real threat API if the client/API extension supplies one. if UnitThreatSituation then local status = UnitThreatSituation("player") if status and status > 0 then return true end end -- Native Vanilla fallback: inspect the player's current hostile target. if UnitTargetsPlayer("target") then return true end -- Improve the old target-only fallback without any dependency: -- inspect hostile targets currently selected by party/raid members. local raidCount = GetRaidCount() local i if raidCount > 0 then for i = 1, raidCount do if UnitTargetsPlayer("raid" .. i .. "target") then return true end end else local partyCount = GetPartyCount() for i = 1, partyCount do if UnitTargetsPlayer("party" .. i .. "target") then return true end end end return false end function HP:CheckAggro() if not DB.AGGRO or UnitIsDead("player") then if state.aggro then state.aggro = false if warningAggro then warningAggro:Hide() end end return end local hasAggro = GetAggroState() if hasAggro and not state.aggro then state.aggro = true if DB.showaggrochat then self:Announce("{rt8} " .. TEXT.aggro_chat) end if DB.showaggroemote and self:CanAnnounce(self:GetCurrentChannel()) then DoSafeEmote("helpme") end if warningAggro then warningAggro:Show() end elseif not hasAggro and state.aggro then state.aggro = false if warningAggro then warningAggro:Hide() end end end -- ============================================================================ -- UI / SETTINGS -- ============================================================================ function HP:CreateGUI() if HPOptionsFrame then return HPOptionsFrame end local f = CreateFrame("Frame", "HPOptionsFrame", UIParent) f:SetWidth(430) f:SetHeight(850) f:SetPoint("CENTER", UIParent, "CENTER") f: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 } }) f:SetMovable(true) f:EnableMouse(true) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() f:StartMoving() end) f:SetScript("OnDragStop", function() f:StopMovingOrSizing() end) f:SetFrameStrata("DIALOG") f:Hide() f.title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") f.title:SetPoint("TOP", 0, -18) f.title:SetText("Healer Protection - Settings") local btnClose = CreateFrame("Button", nil, f, "UIPanelButtonTemplate") btnClose:SetWidth(100) btnClose:SetHeight(24) btnClose:SetPoint("BOTTOM", 0, 16) btnClose:SetText("Close") btnClose:SetScript("OnClick", function() f:Hide() end) local function CreateCheckbox(name, label, key, x, y) local cb = CreateFrame("CheckButton", "HPCB_" .. name, f, "UICheckButtonTemplate") cb:SetPoint("TOPLEFT", x, y) getglobal(cb:GetName() .. "Text"):SetText(label) getglobal(cb:GetName() .. "Text"):SetTextColor(1, 1, 1) cb:SetScript("OnShow", function() cb:SetChecked(DB[key] and 1 or nil) end) cb:SetScript("OnClick", function() DB[key] = cb:GetChecked() == 1 if key == "AGGRO" and not DB.AGGRO then state.aggro = false if warningAggro then warningAggro:Hide() end end end) return cb end local function CreateSlider(name, labelFormat, key, minValue, maxValue, y) local slider = CreateFrame("Slider", "HPSL_" .. name, f, "OptionsSliderTemplate") slider:SetPoint("TOP", 0, y) slider:SetWidth(330) slider:SetHeight(16) slider:SetOrientation("HORIZONTAL") slider:SetMinMaxValues(minValue, maxValue) slider:SetValueStep(1) getglobal(slider:GetName() .. "Low"):SetText(tostring(minValue)) getglobal(slider:GetName() .. "High"):SetText(tostring(maxValue)) local label = getglobal(slider:GetName() .. "Text") local function UpdateLabel(value) label:SetText(string.format(labelFormat, value)) end slider:SetScript("OnShow", function() slider:SetValue(DB[key]) UpdateLabel(DB[key]) end) slider:SetScript("OnValueChanged", function() local value = arg1 if value == nil then value = slider:GetValue() end value = math.floor(value + 0.5) DB[key] = value UpdateLabel(value) end) return slider end local function CreateEditBox(labelText, globalName, x, y, width, key) local label = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") label:SetPoint("TOPLEFT", x, y) label:SetText(labelText) local eb = CreateFrame("EditBox", globalName, f, "InputBoxTemplate") eb:SetWidth(width) eb:SetHeight(20) eb:SetPoint("TOPLEFT", x + 4, y - 18) eb:SetAutoFocus(false) eb:SetScript("OnShow", function() eb:SetText(DB[key] or "") end) local function Save() DB[key] = eb:GetText() or "" end eb:SetScript("OnEnterPressed", function() Save() eb:ClearFocus() end) eb:SetScript("OnEditFocusLost", Save) return eb end local function CreateChannelDropdown(x, y) local label = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") label:SetPoint("TOPLEFT", x, y) label:SetText("Announce Channel") local dropdown = CreateFrame("Frame", "HPChannelDropDown", f, "UIDropDownMenuTemplate") dropdown:SetPoint("TOPLEFT", x - 16, y - 18) UIDropDownMenu_SetWidth(120, dropdown) local channels = { "AUTO", "SAY", "YELL", "PARTY", "RAID", "GUILD" } local function OnClick() DB.channelchat = this.value UIDropDownMenu_SetSelectedValue(dropdown, this.value) UIDropDownMenu_SetText(this.value, dropdown) end local function Initialize() local i for i = 1, table.getn(channels) do local info = {} info.text = channels[i] info.value = channels[i] info.func = OnClick UIDropDownMenu_AddButton(info) end end UIDropDownMenu_Initialize(dropdown, Initialize) UIDropDownMenu_SetSelectedValue(dropdown, DB.channelchat) UIDropDownMenu_SetText(DB.channelchat, dropdown) return dropdown end CreateCheckbox("PrintNothing", "Print Nothing", "printnothing", 20, -45) CreateCheckbox("ShowInRaid", "Show in Raids", "showinraids", 20, -70) CreateCheckbox("Outside", "Show outside instances", "showoutsideofinstance", 20, -95) CreateCheckbox("ShowInBG", "Show in Battlegrounds", "showinbgs", 20, -120) CreateCheckbox("LineOfSight", "Line-of-sight chat warning", "notinsight", 20, -145) CreateCheckbox("Aggro", "AGGRO", "AGGRO", 20, -180) CreateCheckbox("AggroChat", "AGGRO Chat-Message", "showaggrochat", 40, -205) CreateCheckbox("AggroEmote", "AGGRO Emote", "showaggroemote", 40, -230) CreateCheckbox("DeathMessage", "Death message", "deathmessage", 20, -255) CreateCheckbox("OOM", "Out of Mana", "OOM", 20, -290) CreateCheckbox("OOMChat", "OOM Chat-Message", "showoomchat", 40, -315) CreateCheckbox("OOMEmote", "OOM Emote", "showoomemote", 40, -340) CreateSlider("OomPerc", "If under %d%% Mana, OOM alert", "OOMPercentage", 1, 20, -380) CreateCheckbox("NearOOM", "Near out of Mana", "NEAROOM", 20, -430) CreateCheckbox("NearOOMChat", "Near OOM Chat-Message", "shownearoomchat", 40, -455) CreateCheckbox("NearOOMEmote", "Near OOM Emote", "shownearoomemote", 40, -480) CreateSlider("NearOomPerc", "If under %d%% Mana, low-mana alert", "NEAROOMPercentage", 5, 50, -520) CreateCheckbox("NearDeath", "Near Death", "NEARDEATH", 20, -570) CreateCheckbox("NearDeathChat", "Near Death Chat-Message", "showneardeathchat", 40, -595) CreateCheckbox("NearDeathEmote", "Near Death Emote", "showneardeathemote", 40, -620) CreateSlider("NearDeathPerc", "If under %d%% Health, danger alert", "NEARDEATHPercentage", 5, 60, -660) CreateChannelDropdown(20, -710) CreateEditBox("Prefix", "HPEB_Prefix", 20, -770, 175, "prefix") CreateEditBox("Suffix", "HPEB_Suffix", 220, -770, 175, "suffix") return f end SLASH_HEALERPROTECTION1 = "/hp" SLASH_HEALERPROTECTION2 = "/healerprotection" SlashCmdList["HEALERPROTECTION"] = function() if not HPOptionsFrame then HP:CreateGUI() end if HPOptionsFrame:IsVisible() then HPOptionsFrame:Hide() else HPOptionsFrame:Show() end end -- ============================================================================ -- INITIALIZATION / EVENTS -- ============================================================================ local function InitializeDatabase() HPTABPC = HPTABPC or {} DB = HPTABPC local key, value for key, value in pairs(DEFAULTS) do if DB[key] == nil then DB[key] = value end end DB.OOMPercentage = Clamp(DB.OOMPercentage, 1, 20) DB.NEAROOMPercentage = Clamp(DB.NEAROOMPercentage, 5, 50) if DB.NEAROOMPercentage <= DB.OOMPercentage then DB.NEAROOMPercentage = math.min(50, DB.OOMPercentage + 10) end DB.NEARDEATHPercentage = Clamp(DB.NEARDEATHPercentage, 5, 60) end local function CreateAggroWarning() warningAggro = CreateFrame("Frame", nil, UIParent) warningAggro:SetFrameStrata("HIGH") warningAggro:SetWidth(220) warningAggro:SetHeight(40) warningAggro:SetPoint("CENTER", UIParent, "CENTER", 0, 300) warningAggro.text = warningAggro:CreateFontString(nil, "OVERLAY") local font = STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF" warningAggro.text:SetFont(font, 20, "OUTLINE") warningAggro.text:SetPoint("CENTER", warningAggro, "CENTER", 0, 0) warningAggro.text:SetText(TEXT.aggro_warning) warningAggro.text:SetTextColor(1, 0, 0, 1) warningAggro:Hide() end local eventFrame = CreateFrame("Frame") eventFrame:RegisterEvent("PLAYER_LOGIN") eventFrame:RegisterEvent("PLAYER_DEAD") eventFrame:RegisterEvent("PLAYER_ALIVE") eventFrame:RegisterEvent("PLAYER_UNGHOST") eventFrame:RegisterEvent("UNIT_MANA") eventFrame:RegisterEvent("UNIT_MAXMANA") eventFrame:RegisterEvent("UNIT_HEALTH") eventFrame:RegisterEvent("UNIT_MAXHEALTH") eventFrame:RegisterEvent("SPELLCAST_CHANNEL_START") eventFrame:RegisterEvent("SPELLCAST_CHANNEL_STOP") eventFrame:RegisterEvent("SPELLCAST_STOP") eventFrame:RegisterEvent("UI_ERROR_MESSAGE") eventFrame:SetScript("OnEvent", function() local e = event local a1 = arg1 if e == "PLAYER_LOGIN" then InitializeDatabase() CreateAggroWarning() HP:CheckMana() HP:CheckHealth() DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[Healer Protection]|r v1.1 ready. Type |cFFFFFF00/hp|r to configure.") return end if not DB then return end if e == "PLAYER_DEAD" then HP:HandleDeath() return end if e == "PLAYER_ALIVE" or e == "PLAYER_UNGHOST" then HP:HandleAlive() return end if e == "UNIT_MANA" or e == "UNIT_MAXMANA" then if a1 == "player" then HP:CheckMana() end return end if e == "UNIT_HEALTH" or e == "UNIT_MAXHEALTH" then if a1 == "player" then HP:CheckHealth() end return end if e == "SPELLCAST_CHANNEL_START" then state.channeling = true return end if e == "SPELLCAST_CHANNEL_STOP" or e == "SPELLCAST_STOP" then state.channeling = false return end if e == "UI_ERROR_MESSAGE" and DB.notinsight then local errMessage = a1 if errMessage and SPELL_FAILED_LINE_OF_SIGHT and errMessage == SPELL_FAILED_LINE_OF_SIGHT then local targetName = UnitName("target") if targetName and UnitIsFriend("player", "target") then local now = GetTime() if targetName ~= lastLosTarget or now >= nextLosMessage then lastLosTarget = targetName nextLosMessage = now + 1 HP:Announce(TEXT.los .. " (" .. targetName .. ")") end end end end end) -- Aggro is the only feature that requires polling on Vanilla. -- Keep the OnUpdate handler extremely cheap and return immediately when disabled. local aggroFrame = CreateFrame("Frame") local aggroElapsed = 0 aggroFrame:SetScript("OnUpdate", function() if not DB or not DB.AGGRO then return end aggroElapsed = aggroElapsed + (arg1 or 0) if aggroElapsed < 0.75 then return end aggroElapsed = 0 HP:CheckAggro() end)