-- AdePet.lua -- Hunter-only addon for the 1.12 client. -- Shows: Pet_Name: Loyalty_Level (Loyalty_Name) -- and, under it, "Pet is hungry" when the pet's happiness drops below Happy (yellow/red). -- Window hides itself at max loyalty (Level 6). -- -- Commands: -- /hungry X set hunger check interval to X seconds (default 15) -- /adepet lock | unlock | reset (alias: /ap) AdePetDB = AdePetDB or {} local LOYALTY_INTERVAL = 1.0 local DEFAULT_HUNGRY_INTERVAL = 15 local MAX_LOYALTY = 6 local LOYALTY_NAMES = { [1] = "Rebellious", [2] = "Unruly", [3] = "Submissive", [4] = "Dependable", [5] = "Faithful", [6] = "Best Friend", } local isHunter = false local isHungry = false local hungerLevel = 3 -- 1 = unhappy (red), 2 = content (yellow), 3 = happy (green) local frame = CreateFrame("Frame", "AdePetFrame", UIParent) frame:SetWidth(150) frame:SetHeight(28) frame:SetPoint("CENTER", UIParent, "CENTER", 0, 200) frame:SetMovable(true) frame:EnableMouse(true) frame:RegisterForDrag("LeftButton") frame:SetClampedToScreen(true) frame:Hide() frame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) frame:SetBackdropColor(0, 0, 0, 0.7) local text = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") text:SetPoint("TOP", frame, "TOP", 0, -8) text:SetJustifyH("CENTER") local hungryText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") hungryText:SetPoint("TOP", text, "BOTTOM", 0, -3) hungryText:SetJustifyH("CENTER") hungryText:Hide() frame:SetScript("OnDragStart", function() if not AdePetDB.locked then this:StartMoving() end end) frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() local point, _, relPoint, x, y = this:GetPoint() AdePetDB.point = point AdePetDB.relPoint = relPoint AdePetDB.x = x AdePetDB.y = y end) local function GetHungryInterval() return AdePetDB.hungryInterval or DEFAULT_HUNGRY_INTERVAL end -- Fit the window to the visible text lines. local function ResizeToText() local w = text:GetStringWidth() local h1 = text:GetHeight() if not h1 or h1 < 1 then h1 = 12 end local height = 8 + h1 + 8 if hungryText:IsShown() then local w2 = hungryText:GetStringWidth() if w2 > w then w = w2 end local h2 = hungryText:GetHeight() if not h2 or h2 < 1 then h2 = 10 end height = height + h2 + 3 end w = w + 24 if w < 90 then w = 90 end frame:SetWidth(w) frame:SetHeight(height) end -- While the player is flagged AFK, the addon makes no pet queries at all. -- On some servers (seen on OctoWoW) pet API calls count as player activity, -- which clears the AFK flag and makes it flicker on/off while standing still. -- Checks simply resume on the first tick after the AFK flag is gone. local chatAFK = false -- fallback if the client has no UnitIsAFK local function IsPlayerAFK() if UnitIsAFK then return UnitIsAFK("player") and true or false end return chatAFK end -- Reads the pet's happiness. 1 = unhappy, 2 = content, 3 = happy. -- Anything below "happy" is treated as hungry. local function UpdateHunger() if not isHunter or not UnitExists("pet") then isHungry = false return end local happiness = GetPetHappiness() if type(happiness) ~= "number" then happiness = tonumber(happiness) end if happiness and happiness < 3 then isHungry = true hungerLevel = happiness else isHungry = false end end -- Extract numeric loyalty level. Standard API returns a number; some servers -- (e.g. OctoWoW) return text like "(Loyalty Level 1) Rebellious". local function ParseLoyalty(raw) if type(raw) == "number" then return raw elseif type(raw) == "string" then local _, _, digits = string.find(raw, "(%d+)") if digits then return tonumber(digits) end -- fallback: match by rank name local lower = string.lower(raw) for level, name in pairs(LOYALTY_NAMES) do if string.find(lower, string.lower(name), 1, true) then return level end end end return nil end local function UpdateDisplay() if not isHunter then frame:Hide() return end if not UnitExists("pet") then text:SetText("No Pet") hungryText:Hide() ResizeToText() frame:Show() return end local petName = UnitName("pet") or "Pet" local loyaltyLevel = ParseLoyalty(GetPetLoyalty()) -- At max loyalty the window is only shown while the pet is hungry. if loyaltyLevel and loyaltyLevel >= MAX_LOYALTY and not isHungry then frame:Hide() return end if loyaltyLevel then local loyaltyName = LOYALTY_NAMES[loyaltyLevel] or "Unknown" text:SetText(petName .. ": " .. loyaltyLevel .. " (" .. loyaltyName .. ")") else text:SetText(petName) end if isHungry then hungryText:SetText("Pet is hungry") if hungerLevel == 1 then hungryText:SetTextColor(1, 0.2, 0.2) else hungryText:SetTextColor(1, 0.9, 0.1) end hungryText:Show() else hungryText:Hide() end ResizeToText() frame:Show() end local function CheckHunger() UpdateHunger() UpdateDisplay() end -- Feeding detection: while eating, the pet carries the "Feed Pet Effect" buff -- (about 20 sec). We detect it by buff name in a hidden tooltip, with the -- feeding icon as a secondary check. When the buff disappears, re-check hunger -- immediately. local scanTooltip = CreateFrame("GameTooltip", "AdePetScanTooltip", nil, "GameTooltipTemplate") scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") local wasFeeding = false local hungerElapsed = 0 local function PetIsFeeding() if not UnitExists("pet") then return false end for i = 1, 16 do local texture = UnitBuff("pet", i) if not texture then break end if string.find(texture, "BeastTraining", 1, true) then return true end scanTooltip:ClearLines() scanTooltip:SetUnitBuff("pet", i) local line = getglobal("AdePetScanTooltipTextLeft1") local name = line and line:GetText() if name and string.find(string.lower(name), "feed pet", 1, true) then return true end end return false end local function CheckFeeding() local feeding = PetIsFeeding() if wasFeeding and not feeding then -- Feeding just ended: check hunger now, regardless of the timer. hungerElapsed = 0 CheckHunger() end wasFeeding = feeding end -- Timer and event handling live on a separate, always-active frame. A hidden -- frame never receives OnUpdate, so running the timer on the (hideable) window -- would stop all hunger checks at max loyalty, exactly when the window is hidden. local driver = CreateFrame("Frame", "AdePetDriver") local loyaltyElapsed = 0 driver:SetScript("OnUpdate", function() if not isHunter then return end loyaltyElapsed = loyaltyElapsed + arg1 hungerElapsed = hungerElapsed + arg1 -- Work at most once per second. if loyaltyElapsed < LOYALTY_INTERVAL then return end loyaltyElapsed = 0 -- AFK: touch nothing. hungerElapsed keeps growing, so the first tick after -- the AFK flag is gone runs a full hunger check right away. if IsPlayerAFK() then return end if hungerElapsed >= GetHungryInterval() then hungerElapsed = 0 CheckFeeding() CheckHunger() else CheckFeeding() UpdateDisplay() end end) driver:RegisterEvent("PLAYER_ENTERING_WORLD") driver:RegisterEvent("UNIT_PET") driver:RegisterEvent("UNIT_AURA") driver:RegisterEvent("PLAYER_REGEN_ENABLED") -- left combat driver:RegisterEvent("CHAT_MSG_SYSTEM") -- only used for the AFK fallback driver:SetScript("OnEvent", function() if event == "CHAT_MSG_SYSTEM" then if arg1 then if string.find(arg1, "now AFK", 1, true) then chatAFK = true elseif string.find(arg1, "no longer AFK", 1, true) then chatAFK = false end end return end if event == "UNIT_AURA" then if isHunter and arg1 == "pet" and not IsPlayerAFK() then CheckFeeding() end return end if event == "PLAYER_ENTERING_WORLD" then local _, englishClass = UnitClass("player") isHunter = (englishClass == "HUNTER") if not isHunter then frame:Hide() return end if AdePetDB.point then frame:ClearAllPoints() frame:SetPoint(AdePetDB.point, UIParent, AdePetDB.relPoint, AdePetDB.x, AdePetDB.y) end end if not isHunter then return end if event == "UNIT_PET" and arg1 ~= "player" then return end -- While AFK, don't query the pet: just make sure the next non-AFK tick -- runs a hunger check. if IsPlayerAFK() then hungerElapsed = GetHungryInterval() return end -- Entering world, pet change, or leaving combat: check hunger right away. hungerElapsed = 0 CheckHunger() end) SLASH_ADEPETHUNGRY1 = "/hungry" SlashCmdList["ADEPETHUNGRY"] = function(msg) if not isHunter then DEFAULT_CHAT_FRAME:AddMessage("AdePet: this addon only works for Hunters.") return end local seconds = tonumber(msg) if not seconds or seconds < 1 then DEFAULT_CHAT_FRAME:AddMessage("AdePet: hunger check every " .. GetHungryInterval() .. "s. Usage: /hungry X (X = seconds, minimum 1)") return end AdePetDB.hungryInterval = seconds hungerElapsed = 0 DEFAULT_CHAT_FRAME:AddMessage("AdePet: hunger check interval set to " .. seconds .. "s.") end SLASH_ADEPET1 = "/adepet" SLASH_ADEPET2 = "/ap" SlashCmdList["ADEPET"] = function(msg) msg = string.lower(msg or "") if not isHunter then DEFAULT_CHAT_FRAME:AddMessage("AdePet: this addon only works for Hunters.") return end if msg == "lock" then AdePetDB.locked = true DEFAULT_CHAT_FRAME:AddMessage("AdePet: window locked.") elseif msg == "unlock" then AdePetDB.locked = false DEFAULT_CHAT_FRAME:AddMessage("AdePet: window unlocked (drag to move).") elseif msg == "reset" then AdePetDB.point = nil frame:ClearAllPoints() frame:SetPoint("CENTER", UIParent, "CENTER", 0, 200) DEFAULT_CHAT_FRAME:AddMessage("AdePet: position reset.") elseif msg == "debug" then -- Prints the raw values the client returns, for troubleshooting. local happiness, damage, rate = GetPetHappiness() DEFAULT_CHAT_FRAME:AddMessage("AdePet debug: GetPetHappiness = " .. tostring(happiness) .. " (" .. type(happiness) .. "), " .. tostring(damage) .. ", " .. tostring(rate)) local loyalty = GetPetLoyalty() DEFAULT_CHAT_FRAME:AddMessage("AdePet debug: GetPetLoyalty = " .. tostring(loyalty) .. " (" .. type(loyalty) .. "), parsed level = " .. tostring(ParseLoyalty(loyalty))) DEFAULT_CHAT_FRAME:AddMessage("AdePet debug: hungry = " .. tostring(isHungry) .. ", feeding = " .. tostring(PetIsFeeding()) .. ", interval = " .. GetHungryInterval() .. "s, window shown = " .. tostring(frame:IsShown()) .. ", AFK = " .. tostring(IsPlayerAFK()) .. " (UnitIsAFK " .. (UnitIsAFK and "available" or "missing") .. ")") else DEFAULT_CHAT_FRAME:AddMessage("AdePet commands: /adepet lock, unlock, reset, debug | /hungry X") end end