-- ========================================================================== -- Heal Box Vanilla -- -- Original: "Heal Box" von Dourd (Argent Dawn EU) - UI Overhauled. -- Portierung auf Vanilla 1.12 / Client 1.21.1 (optional SuperWoW) sowie -- Ausbau des Funktionsumfangs 09/2026 durch Mquadrat: -- * eigenes Kaskadenmenue fuer die Zauber- und Rangwahl -- * Heilvorhersage fuer Direktheilung, HoT-Restticks und Absorb-Schilde, -- selbstkorrigierend ueber den Combatlog -- * HealComm-Sync mit Puppeteer, pfUI, Luna und Co. -- * Lokalisierung Deutsch / Englisch, im Optionsfenster umschaltbar -- -- Ehre wem Ehre gebuehrt: Aufbau, Namensplaketten und Grundidee stammen -- aus dem Original. -- ========================================================================== FBHasSuperWoW = (SUPERWOW_VERSION ~= nil); FBClass = UnitClass("player"); -- [[ Globals ]] -- HealBox = { MaxButtons = 5, Scale = 1.0, AttachMode = 0, Active = 1, SpellChoice = {}, }; -- Anzeigename des Addons. FBADDON_FOLDER muss dem Ordnernamen unter -- Interface\AddOns entsprechen (dort liegt auch die .toc) - nur dann -- feuert ADDON_LOADED fuer uns. FBADDON_NAME = "Heal Box Vanilla"; FBADDON_FOLDER = "FBHealBox"; HealBoxVersion = "|cFFFFFF00v1.4|r"; -- ========================================================================== -- [ Lokalisierung / Localization ] -- -- Alle sichtbaren Texte liegen in FBLocale. Die aktive Sprache haengt in -- FBL, abgefragt wird ueber FBT("SCHLUESSEL"). Fehlt ein Schluessel in der -- gewaehlten Sprache, faellt er auf Englisch zurueck. -- -- Umgeschaltet wird im Optionsfenster; FBHealBox_ApplyLocale() beschriftet -- die bereits gebaute Oberflaeche neu, ein /reload ist nicht noetig. -- -- Hinweis: In den angezeigten Texten bewusst ae/oe/ue/ss statt Umlauten - -- der 1.12-Client behandelt Umlaute je nach Locale und Schriftart -- unterschiedlich. -- ========================================================================== FBLocale = {}; FBLocale["enUS"] = { LANG_NAME = "English", LANGUAGE = "Language", LOADED = "loaded|r - the minimap button opens the options, /fbp reports the heal prediction.", SUPERWOW = " |cFF55FF55[SuperWoW detected]|r", CREDITS = "|cFFAAAAAAOriginal by Dourd (Argent Dawn EU) - ported to Vanilla and extended 09/2026 by Mquadrat|r", TT_NO_SPELL = "|cFFFFFFFFNo spell\n|cFF00FF00Pick a spell in the options window.", TT_TARGET = "Target", NOT_IN_GROUP = " is not in your group.", SELECT_SPELL = "Select spell...", BUTTON = "Button", MENU_NO_SPELL = "|cFF999999No spell|r", RANK_DEFAULT = "Default", PANEL_SUB = "Options for %s.\nChoose how many buttons to show\nand which spell each button casts.", SHOW_BUTTONS = "Show |cFFFFFFFF%s|r buttons", SCALE = "Frame scale: |cFFFFFFFF%s", SMALL = "Small", LARGE = "Large", ATTACH = "Default party frames", ATTACH_TIP = "Attaches the heal buttons to Blizzard's default party frames instead of using the addon's own movable name plates.", COMM = "HealComm sync", COMM_TIP = "Broadcasts your heals in the HealComm format so Puppeteer, pfUI, Luna and others can display them, and feeds the heals announced by other healers into your own prediction.", LANG_TIP = "Switches every text in the addon. Takes effect immediately, no reload required.", ABOUT = "%s %s |cFFAAAAAA- original by Dourd, UI Overhauled|r\n|cFFAAAAAAPorted to Vanilla and extended 09/2026 by Mquadrat|r", MM_TIP = "Left: options\nHold right: move this button\nShift + left: show/hide the display", FBP_WATCHED = "Spells read from the spellbook:", FBP_ACTIVE = "Currently active:", FBP_DIRECT = "direct", FBP_SHIELD = "shield", FBP_LEARNED = "learned", FBP_EVERY = "every", FBP_TICKSOF = "ticks of", FBP_CAST = "Cast", FBP_ON = "on", FBP_OF = "of", FBP_FOR = "for", FBP_SYNC = "HealComm sync:", FBP_STATE_ON = "on", FBP_STATE_OFF = "off", FBP_INCOMING = "Incoming", FBP_DEBUG = "Debug:", FBP_RESET = "Learned values discarded.", DBG_HOT = "HoT %s on %s: %d per tick, %ds", DBG_SHIELD = "Shield %s on %s: %d absorb, %ds", DBG_CAST = "Cast %s on %s: %d", DBG_TICK = "Tick corrected: %s = %d", DBG_HEAL = "Heal %s = %d (estimate now %d)", DBG_ABSORB = "Absorb %d on %s (%d left)", }; FBLocale["deDE"] = { LANG_NAME = "Deutsch", LANGUAGE = "Sprache", LOADED = "geladen|r - der Minimap-Button oeffnet die Optionen, /fbp zeigt den Stand der Heilvorhersage.", SUPERWOW = " |cFF55FF55[SuperWoW erkannt]|r", CREDITS = "|cFFAAAAAAOriginal von Dourd (Argent Dawn EU) - Vanilla-Portierung und Erweiterung 09/2026 von Mquadrat|r", TT_NO_SPELL = "|cFFFFFFFFKein Zauber\n|cFF00FF00Waehle in den Optionen einen Zauber aus.", TT_TARGET = "Ziel", NOT_IN_GROUP = " ist nicht in der Gruppe.", SELECT_SPELL = "Zauber waehlen...", BUTTON = "Button", MENU_NO_SPELL = "|cFF999999Kein Zauber|r", RANK_DEFAULT = "Standard", PANEL_SUB = "Optionen fuer %s.\nUnten legst du fest, wie viele Buttons erscheinen\nund welcher Zauber auf welchem Button liegt.", SHOW_BUTTONS = "|cFFFFFFFF%s|r Buttons anzeigen", SCALE = "Skalierung: |cFFFFFFFF%s", SMALL = "Klein", LARGE = "Gross", ATTACH = "Standard-Gruppenfenster", ATTACH_TIP = "Heftet die Heil-Buttons an Blizzards Standard-Gruppenfenster, statt eigene, frei platzierbare Namensplaketten zu verwenden.", COMM = "HealComm-Sync", COMM_TIP = "Sendet deine Heilungen im HealComm-Format an die Gruppe (Puppeteer, pfUI, Luna und Co. zeigen sie an) und uebernimmt umgekehrt die angekuendigten Heilungen anderer Heiler in die eigene Vorhersage.", LANG_TIP = "Stellt alle Texte des Addons um. Wirkt sofort, ein /reload ist nicht noetig.", ABOUT = "%s %s |cFFAAAAAA- Original von Dourd, UI Overhauled|r\n|cFFAAAAAAVanilla-Portierung und Erweiterung 09/2026 von Mquadrat|r", MM_TIP = "Links: Optionen\nRechts halten: Button verschieben\nShift + Links: Anzeige ein/aus", FBP_WATCHED = "Ausgelesene Zauber:", FBP_ACTIVE = "Aktiv:", FBP_DIRECT = "Direkt", FBP_SHIELD = "Schild", FBP_LEARNED = "gelernt", FBP_EVERY = "alle", FBP_TICKSOF = "Ticks a", FBP_CAST = "Cast", FBP_ON = "auf", FBP_OF = "von", FBP_FOR = "fuer", FBP_SYNC = "HealComm-Sync:", FBP_STATE_ON = "an", FBP_STATE_OFF = "aus", FBP_INCOMING = "Fremdheilung", FBP_DEBUG = "Debug:", FBP_RESET = "Gelernte Werte verworfen.", DBG_HOT = "HoT %s auf %s: %d pro Tick, %ds", DBG_SHIELD = "Schild %s auf %s: %d Absorb, %ds", DBG_CAST = "Cast %s auf %s: %d", DBG_TICK = "Tick korrigiert: %s = %d", DBG_HEAL = "Heilung %s = %d (Schaetzung jetzt %d)", DBG_ABSORB = "Absorb %d auf %s (Rest %d)", }; FBL = nil; -- Client-Sprache als Vorgabe function FBDetectLocale() if (GetLocale and GetLocale() == "deDE") then return "deDE"; end return "enUS"; end -- Textabfrage mit Rueckfall auf Englisch function FBT(key) local t = FBL or FBLocale["enUS"]; local s = t[key]; if (s == nil) then s = FBLocale["enUS"][key]; end if (s == nil) then return key; end return s; end function FBSetLocale(code, apply) if (not code) or (not FBLocale[code]) then code = "enUS"; end FBL = FBLocale[code]; if (HealBox) then HealBox.Locale = code; end if (apply) then FBHealBox_ApplyLocale(); end end FBL = FBLocale[FBDetectLocale()]; xSpacing = 2; LowHP = 0.6; VeryLowHP = 0.3; NamePlateWidth = 120; NamePlateHeight = 28; FBDropDown = {}; FBDropDownButtonValue = {}; FBDropDownButton = {}; FBDropDownButtonIcon = {}; ClassIcon = { Druid = "Interface/Icons/INV_Misc_MonsterClaw_04", Warlock = "Interface/Icons/Spell_Nature_FaerieFire", Hunter = "Interface/Icons/INV_Weapon_Bow_07", Mage = "Interface/Icons/INV_Staff_13", Priest = "Interface/Icons/INV_Staff_30", Warrior = "Interface/Icons/INV_Sword_27", Shaman = "Interface/Icons/Spell_Nature_BloodLust", Paladin = "Interface/Icons/Ability_ThunderBolt", Rogue = "Interface/AddOns/ChatIcons/images/UI-CharacterCreate-Classes_Rogue", } Spell = { Name = {}, Icon = {}, ID = {}, }; if (FBClass == "Druid") then Spell.Name[1] = "Rejuvenation"; Spell.Name[2] = "Regrowth"; Spell.Name[3] = "Lifebloom"; Spell.Name[4] = "Healing Touch"; Spell.Name[5] = "Swiftmend"; Spell.Name[6] = "Remove Curse"; Spell.Name[7] = "Abolish Poison"; end if (FBClass == "Priest") then Spell.Name[1] = "Renew"; Spell.Name[2] = "Flash Heal"; Spell.Name[3] = "Lesser Heal"; Spell.Name[4] = "Heal"; Spell.Name[5] = "Greater Heal"; Spell.Name[6] = "Binding Heal"; Spell.Name[7] = "Prayer of Healing"; Spell.Name[8] = "Prayer of Mending"; Spell.Name[9] = "Circle of Healing"; Spell.Name[10] = "Power Word: Shield"; Spell.Name[11] = "Abolish Disease"; Spell.Name[12] = "Cure Disease"; Spell.Name[13] = "Dispel Magic"; end if (FBClass == "Shaman") then Spell.Name[1] = "Lesser Healing Wave"; Spell.Name[2] = "Healing Wave"; Spell.Name[3] = "Chain Heal"; Spell.Name[4] = "Earth Shield"; Spell.Name[5] = "Cure Poison"; Spell.Name[6] = "Cure Disease"; end if (FBClass == "Paladin") then Spell.Name[1] = "Flash of Light"; Spell.Name[2] = "Holy Light"; Spell.Name[3] = "Holy Shock"; Spell.Name[4] = "Lay on Hands"; Spell.Name[5] = "Purify"; Spell.Name[6] = "Cleanse"; Spell.Name[7] = "Blessing of Protection"; end FBParty1 = {}; FBParty2 = {}; FBParty3 = {}; FBParty4 = {}; FBParty5 = {}; MaxButtonCount = 10; -- Speichert alle verfügbaren Zauber und Ränge FBPlayerSpells = {}; FBActiveSpellIDs = {}; FBSpellBtns = {}; function FBHealBox_OnLoad() local swowTag = ""; if (FBHasSuperWoW) then swowTag = FBT("SUPERWOW"); end DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME.."|r "..HealBoxVersion.." : |cFF00FF00"..FBT("LOADED")..swowTag); DEFAULT_CHAT_FRAME:AddMessage(FBT("CREDITS")); this:RegisterEvent("ADDON_LOADED"); this:RegisterEvent("PARTY_MEMBERS_CHANGED"); this:RegisterEvent("PLAYER_ENTERING_WORLD"); this:RegisterEvent("SPELLS_CHANGED"); this:RegisterEvent("UNIT_HEALTH"); this:RegisterEvent("SPELL_UPDATE_COOLDOWN"); this:RegisterEvent("VARIABLES_LOADED"); this:RegisterEvent("UNIT_AURA"); end function FBUnitGUID(unit) if (FBHasSuperWoW and UnitExists(unit)) then local exists, guid = UnitExists(unit); return guid; end return nil; end -- [ Zauberbuch scannen und Ränge sammeln ] -- function FBLoadSpellData() FBPlayerSpells = {}; local i = 1; while true do local spellName, spellRank = GetSpellName(i, BOOKTYPE_SPELL); if not spellName then break; end local isHealBoxSpell = false; for _, v in ipairs(Spell.Name) do if v == spellName then isHealBoxSpell = true; break; end end if isHealBoxSpell then if not FBPlayerSpells[spellName] then FBPlayerSpells[spellName] = {}; end local icon = GetSpellTexture(i, BOOKTYPE_SPELL); table.insert(FBPlayerSpells[spellName], { rank = spellRank, id = i, icon = icon }); end i = i + 1; end -- Tooltips auswerten: welche dieser Zauber sind HoTs bzw. Absorb-Schilde? FBPredict_BuildWatch(); for btnIndex = 1, MaxButtonCount, 1 do local saved = HealBox.SpellChoice[btnIndex]; if type(saved) == "number" then HealBox.SpellChoice[btnIndex] = nil; saved = nil; end FBApplySpellChoice(btnIndex, saved); end end function FBApplySpellChoice(i, castString) if type(castString) == "number" then return; end FBDropDownButton[i] = castString; FBDropDownButtonIcon[i] = "Interface\\Icons\\INV_Misc_QuestionMark"; FBActiveSpellIDs[i] = nil; if castString then for baseName, ranks in pairs(FBPlayerSpells) do for _, spellData in ipairs(ranks) do local cmpString = baseName; if spellData.rank and spellData.rank ~= "" then cmpString = baseName .. "(" .. spellData.rank .. ")"; end if cmpString == castString then FBDropDownButtonIcon[i] = spellData.icon; FBActiveSpellIDs[i] = spellData.id; break; end end end end if FBSpellBtns[i] then if castString and FBDropDownButtonIcon[i] then FBSpellBtns[i].text:SetText(castString); FBSpellBtns[i].icon:SetTexture(FBDropDownButtonIcon[i]); FBSpellBtns[i].icon:Show(); else FBSpellBtns[i].text:SetText(FBT("SELECT_SPELL")); FBSpellBtns[i].icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark"); FBSpellBtns[i].icon:Show(); end end end -- ========================================================================== -- [ FB Cascading Menu - eigenes Menuesystem fuer Vanilla 1.12 / Lua 5.0 ] -- -- Warum kein UIDropDownMenu mehr? -- * 1.12 loest Level-2 ueber UIDROPDOWNMENU_OPEN_MENU (ein STRING) auf und -- ruft dort ToggleDropDownMenu(level+1, ...) ohne Frame-Referenz auf -> -- bei Custom-Frames im displayMode "MENU" bricht die Kette regelmaessig. -- * UIDropDownMenuButton_OnEnter ruft bei JEDEM Eintrag ohne hasArrow -- CloseDropDownMenus(level+1) -> das Untermenue stirbt, sobald die Maus -- ueber einen Nachbareintrag wandert. -- * info.func + keepShownOnClick erzeugt zwangsweise Haken + Check-Sound. -- -- Dieses Menue: Hover oeffnet, nichts schliesst es ausser Auswahl / anderes -- Untermenue / Klick daneben. Icon links, Text linksbuendig, kein Sound. -- ========================================================================== FBMENU_MAX_LEVELS = 2; -- Anzahl Menue-Ebenen FBMENU_BTN_HEIGHT = 17; -- Zeilenhoehe FBMENU_ICON_SIZE = 16; -- Icon-Kantenlaenge FBMENU_TEXT_GAP = 6; -- Abstand Icon -> Text FBMENU_PAD = 8; -- Innenabstand der Liste FBMENU_ARROW_SPACE = 18; -- Platz fuer den Pfeil rechts FBMENU_MOUSE_PAD = 14; -- Toleranzzone: Bruecke zwischen L1 und L2 FBMENU_GRACE_TIME = 3.0; -- Sek. ausserhalb -> Auto-Close (999 = aus) FBMenuList = {}; -- [level] = Frame FBMenuActiveButtonID = 1; -- welcher Options-Slot wird gerade belegt FBMenuOpenSubValue = nil; -- welcher Zauber haengt gerade im Untermenue FBMenuOutTimer = 0; -- Treiber fuer den Auto-Close (OnUpdate laeuft nur solange sichtbar) FBMenuDriver = CreateFrame("Frame", "FBMenuDriver", UIParent); FBMenuDriver:Hide(); FBMenuDriver:SetScript("OnUpdate", function() FBMenu_OnUpdate(arg1); end); -- Unsichtbarer Klickfaenger hinter dem Menue (Klick daneben schliesst) FBMenuCloser = CreateFrame("Button", "FBMenuCloser", UIParent); FBMenuCloser:SetAllPoints(UIParent); FBMenuCloser:SetFrameStrata("FULLSCREEN"); FBMenuCloser:EnableMouse(true); FBMenuCloser:RegisterForClicks("LeftButtonUp", "RightButtonUp"); FBMenuCloser:SetScript("OnClick", function() FBMenu_CloseAll(); end); FBMenuCloser:Hide(); -- [ Hilfsfunktionen ] ------------------------------------------------------ function FBMenu_MouseOver(frame) if (not frame) or (not frame:IsVisible()) then return false; end local left = frame:GetLeft(); if (not left) then return false; end local scale = frame:GetEffectiveScale(); local x, y = GetCursorPosition(); x = x / scale; y = y / scale; local pad = FBMENU_MOUSE_PAD; return (x >= left - pad) and (x <= frame:GetRight() + pad) and (y >= frame:GetBottom() - pad) and (y <= frame:GetTop() + pad); end function FBMenu_GetList(level) if (FBMenuList[level]) then return FBMenuList[level]; end local f = CreateFrame("Frame", "FBMenuList"..level, UIParent); f:SetFrameStrata("FULLSCREEN_DIALOG"); f:SetFrameLevel(10 + level * 5); f:SetToplevel(true); f:EnableMouse(true); f:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 4, right = 4, top = 4, bottom = 4 } }); f:SetBackdropColor(0, 0, 0, 0.92); f.buttons = {}; f:Hide(); FBMenuList[level] = f; return f; end function FBMenu_GetButton(list, index) if (list.buttons[index]) then return list.buttons[index]; end local yOff = -FBMENU_PAD - ((index - 1) * FBMENU_BTN_HEIGHT); local b = CreateFrame("Button", list:GetName().."Button"..index, list); b:SetHeight(FBMENU_BTN_HEIGHT); b:SetPoint("TOPLEFT", list, "TOPLEFT", FBMENU_PAD, yOff); b:SetPoint("TOPRIGHT", list, "TOPRIGHT", -FBMENU_PAD, yOff); b.icon = b:CreateTexture(nil, "ARTWORK"); b.icon:SetWidth(FBMENU_ICON_SIZE); b.icon:SetHeight(FBMENU_ICON_SIZE); b.icon:SetPoint("LEFT", b, "LEFT", 0, 0); b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93); b.text = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); b.text:SetPoint("LEFT", b, "LEFT", FBMENU_ICON_SIZE + FBMENU_TEXT_GAP, 0); b.text:SetJustifyH("LEFT"); b.arrow = b:CreateTexture(nil, "ARTWORK"); b.arrow:SetWidth(16); b.arrow:SetHeight(16); b.arrow:SetPoint("RIGHT", b, "RIGHT", 2, 0); b.arrow:SetTexture("Interface\\ChatFrame\\ChatFrameExpandArrow"); b.arrow:Hide(); b.hl = b:CreateTexture(nil, "HIGHLIGHT"); b.hl:SetAllPoints(b); b.hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight"); b.hl:SetBlendMode("ADD"); b:RegisterForClicks("LeftButtonUp"); b:SetScript("OnEnter", function() FBMenu_ButtonOnEnter(); end); b:SetScript("OnClick", function() FBMenu_ButtonOnClick(); end); list.buttons[index] = b; return b; end -- [ Menue-Ebene aufbauen und anzeigen ] ------------------------------------ -- entries = Array aus { text, icon, hasArrow, submenu, value, spellID, func, isTitle } -- anchor = Level 1: der Options-Button; Level >1: der Eintrag mit dem Pfeil function FBMenu_ShowLevel(level, entries, anchor) local list = FBMenu_GetList(level); local count = table.getn(entries); if (count == 0) then list:Hide(); return; end local maxText = 0; local anyArrow = false; for i = 1, count do local e = entries[i]; local b = FBMenu_GetButton(list, i); b.entry = e; b.level = level; b.text:SetText(e.text); if (e.isTitle) then b.text:SetTextColor(1, 0.82, 0); b.hl:SetAlpha(0); else b.text:SetTextColor(1, 1, 1); b.hl:SetAlpha(1); end if (e.icon) then b.icon:SetTexture(e.icon); b.icon:Show(); else b.icon:Hide(); end if (e.hasArrow) then b.arrow:Show(); anyArrow = true; else b.arrow:Hide(); end b:Show(); local w = b.text:GetStringWidth(); if (w > maxText) then maxText = w; end end -- ueberzaehlige Buttons aus einem frueheren Aufruf verstecken local j = count + 1; while (list.buttons[j]) do list.buttons[j]:Hide(); j = j + 1; end local width = (FBMENU_PAD * 2) + FBMENU_ICON_SIZE + FBMENU_TEXT_GAP + maxText; if (anyArrow) then width = width + FBMENU_ARROW_SPACE; end if (width < 130) then width = 130; end list:SetWidth(width); list:SetHeight((FBMENU_PAD * 2) + (count * FBMENU_BTN_HEIGHT)); list:ClearAllPoints(); if (level == 1) then list:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -2); else -- 2px Ueberlappung mit der Elternliste: kein Loch, das die Maus verliert list:SetPoint("TOPLEFT", anchor, "TOPRIGHT", FBMENU_PAD - 2, FBMENU_PAD); end list:Show(); -- Ragt es rechts aus dem Bildschirm? Dann nach links klappen. if (level > 1) and list:GetRight() and (list:GetRight() > UIParent:GetRight()) then list:ClearAllPoints(); list:SetPoint("TOPRIGHT", anchor, "TOPLEFT", -(FBMENU_PAD - 2), FBMENU_PAD); end end -- [ Maus-Handler ] --------------------------------------------------------- function FBMenu_ButtonOnEnter() local e = this.entry; if (not e) or e.isTitle then return; end if (e.hasArrow) then -- Hover oeffnet das Untermenue (nur neu bauen, wenn ein anderer Zauber) if (FBMenuOpenSubValue ~= e.submenu) then FBMenuOpenSubValue = e.submenu; FBMenu_ShowLevel(this.level + 1, FBMenu_BuildRankEntries(e.submenu), this); end end -- Eintraege OHNE Pfeil lassen ein offenes Untermenue absichtlich stehen. -- (Blizzards Original wuerde hier CloseDropDownMenus() feuern.) end function FBMenu_ButtonOnClick() local e = this.entry; if (not e) or e.isTitle then return; end if (e.hasArrow) then FBMenuOpenSubValue = e.submenu; FBMenu_ShowLevel(this.level + 1, FBMenu_BuildRankEntries(e.submenu), this); return; end if (e.func) then e.func(e); end end function FBMenu_OnUpdate(elapsed) if (not elapsed) then return; end local over = false; for lvl = 1, FBMENU_MAX_LEVELS do if (FBMenu_MouseOver(FBMenuList[lvl])) then over = true; break; end end if (over) then FBMenuOutTimer = 0; else FBMenuOutTimer = FBMenuOutTimer + elapsed; if (FBMenuOutTimer > FBMENU_GRACE_TIME) then FBMenu_CloseAll(); end end end -- [ Oeffnen / Schliessen ] ------------------------------------------------- function FBMenu_IsOpen() return (FBMenuList[1] and FBMenuList[1]:IsVisible()); end function FBMenu_CloseAll() for lvl = 1, FBMENU_MAX_LEVELS do if (FBMenuList[lvl]) then FBMenuList[lvl]:Hide(); end end FBMenuOpenSubValue = nil; FBMenuOutTimer = 0; FBMenuDriver:Hide(); FBMenuCloser:Hide(); end -- Beliebige Eintragsliste als Menue oeffnen (Zauberwahl, Sprachwahl, ...) function FBMenu_OpenMenu(entries, anchorFrame) FBMenu_CloseAll(); FBMenu_ShowLevel(1, entries, anchorFrame); FBMenuOutTimer = 0; FBMenuCloser:Show(); FBMenuDriver:Show(); end function FBMenu_OpenSpellMenu(btnIndex, anchorFrame) -- gleicher Slot nochmal geklickt -> zu if (FBMenu_IsOpen() and FBMenuActiveButtonID == btnIndex) then FBMenu_CloseAll(); return; end FBMenuActiveButtonID = btnIndex; FBMenu_OpenMenu(FBMenu_BuildSpellEntries(), anchorFrame); end -- Sprachumschaltung im Optionsfenster function FBMenu_OpenLanguageMenu(anchorFrame) if (FBMenu_IsOpen()) then FBMenu_CloseAll(); return; end local entries = {}; local codes = { "deDE", "enUS" }; for _, code in ipairs(codes) do local e = {}; e.text = FBLocale[code].LANG_NAME; e.code = code; e.func = function(entry) FBSetLocale(entry.code, true); FBMenu_CloseAll(); end table.insert(entries, e); end FBMenu_OpenMenu(entries, anchorFrame); end -- [ Inhalte ] -------------------------------------------------------------- function FBMenu_MakeCastString(spellName, rank) if (rank and rank ~= "") then return spellName.."("..rank..")"; end return spellName; end function FBMenu_BuildSpellEntries() local entries = {}; local clear = {}; clear.text = FBT("MENU_NO_SPELL"); clear.icon = "Interface\\Icons\\INV_Misc_QuestionMark"; clear.func = FBMenu_ClearSpell; table.insert(entries, clear); for _, spellName in ipairs(Spell.Name) do local ranks = FBPlayerSpells[spellName]; if (ranks) then local numRanks = table.getn(ranks); local e = {}; e.text = spellName; e.icon = ranks[1].icon; if (numRanks > 1) then e.hasArrow = true; e.submenu = spellName; else e.value = FBMenu_MakeCastString(spellName, ranks[1].rank); e.spellID = ranks[1].id; e.func = FBMenu_SelectSpell; end table.insert(entries, e); end end return entries; end function FBMenu_BuildRankEntries(spellName) local entries = {}; local ranks = FBPlayerSpells[spellName]; if (not ranks) then return entries; end local title = {}; title.text = spellName; title.isTitle = true; title.icon = ranks[1].icon; table.insert(entries, title); for _, sd in ipairs(ranks) do local e = {}; e.text = sd.rank; if (not e.text) or (e.text == "") then e.text = FBT("RANK_DEFAULT"); end e.icon = sd.icon; e.value = FBMenu_MakeCastString(spellName, sd.rank); e.spellID = sd.id; e.func = FBMenu_SelectSpell; table.insert(entries, e); end return entries; end function FBMenu_SelectSpell(entry) local btnID = FBMenuActiveButtonID; HealBox.SpellChoice[btnID] = entry.value; FBApplySpellChoice(btnID, entry.value); FBHealBoxButtonsChanged(); FBMenu_CloseAll(); end function FBMenu_ClearSpell() local btnID = FBMenuActiveButtonID; HealBox.SpellChoice[btnID] = nil; FBApplySpellChoice(btnID, nil); FBHealBoxButtonsChanged(); FBMenu_CloseAll(); end function FBHealBox_OnEvent(event, arg1) if ((event == "ADDON_LOADED") and (arg1 == FBADDON_FOLDER)) then if (not HealBox.SpellChoice) then HealBox.SpellChoice = {}; end if (HealBox.Active == nil) then HealBox.Active = 1; end if (HealBox.HealComm == nil) then HealBox.HealComm = 1; end if (HealBox.Locale == nil) then HealBox.Locale = FBDetectLocale(); end FBSetLocale(HealBox.Locale, 1); FBUpdateNames(); MaxButtonSlider:SetValue(HealBox.MaxButtons); HealBoxAttachMode(HealBox.AttachMode); end if (event == "PLAYER_ENTERING_WORLD" or event == "SPELLS_CHANGED") then FBLoadSpellData(); FBHealBoxButtons(); if (FBRegisterMinimapButtonWithMBB) then FBRegisterMinimapButtonWithMBB(); end FBHealBox_RefreshAllBars(); end if (event == "VARIABLES_LOADED") then if (not HealBox.SpellChoice) then HealBox.SpellChoice = {}; end if (HealBox.HealComm == nil) then HealBox.HealComm = 1; end if (HealBox.Locale == nil) then HealBox.Locale = FBDetectLocale(); end FBSetLocale(HealBox.Locale, 1); FBLoadSpellData(); FBHealBoxButtons(); HealBoxScale(FBHealBox1, HealBox.Scale); ScaleSlider:SetValue(HealBox.Scale); AttachModeCheck:SetChecked(HealBox.AttachMode); HealCommCheck:SetChecked(HealBox.HealComm == 1); FBUpdateNames(); if (HealBox.AttachMode == 0) then FBHealBox1:SetScale(HealBox.Scale); else FBHealBox1:SetScale(1); end end if (event == "PARTY_MEMBERS_CHANGED") then FBUpdateNames(); end if ((event == "UNIT_HEALTH" ) and (arg1 == "player")) then HPPercent = UnitHealth("player") / UnitHealthMax("player"); FBHealBox1.HPText:SetText(format("%d%%", HPPercent*100)); FBHealBox1.HealthBar:SetMinMaxValues(0, UnitHealthMax("player")); FBHealBox1.HealthBar:SetValue(UnitHealth("player")); if (HPPercent > LowHP) then FBHealBox1.HealthBar:SetStatusBarColor(0,1,0,1); end; if (HPPercent < LowHP) then FBHealBox1.HealthBar:SetStatusBarColor(1,0.9,0,1); end; if (HPPercent < VeryLowHP) then FBHealBox1.HealthBar:SetStatusBarColor(1,0,0,1); end; end if ((event == "UNIT_HEALTH" ) and (arg1 == "party1")) then HPPercent = UnitHealth("party1") / UnitHealthMax("party1"); FBHealBox2.HPText:SetText(format("%d%%", HPPercent*100)); FBHealBox2.HealthBar:SetMinMaxValues(0, UnitHealthMax("party1")); FBHealBox2.HealthBar:SetValue(UnitHealth("party1")); if (HPPercent > LowHP) then FBHealBox2.HealthBar:SetStatusBarColor(0,1,0,1); end; if (HPPercent < LowHP) then FBHealBox2.HealthBar:SetStatusBarColor(1,0.9,0,1); end; if (HPPercent < VeryLowHP) then FBHealBox2.HealthBar:SetStatusBarColor(1,0,0,1); end; end if ((event == "UNIT_HEALTH" ) and (arg1 == "party2")) then HPPercent = UnitHealth("party2") / UnitHealthMax("party2"); FBHealBox3.HPText:SetText(format("%d%%", HPPercent*100)); FBHealBox3.HealthBar:SetMinMaxValues(0, UnitHealthMax("party2")); FBHealBox3.HealthBar:SetValue(UnitHealth("party2")); if (HPPercent > LowHP) then FBHealBox3.HealthBar:SetStatusBarColor(0,1,0,1); end; if (HPPercent < LowHP) then FBHealBox3.HealthBar:SetStatusBarColor(1,0.9,0,1); end; if (HPPercent < VeryLowHP) then FBHealBox3.HealthBar:SetStatusBarColor(1,0,0,1); end; end if ((event == "UNIT_HEALTH" ) and (arg1 == "party3")) then HPPercent = UnitHealth("party3") / UnitHealthMax("party3"); FBHealBox4.HPText:SetText(format("%d%%", HPPercent*100)); FBHealBox4.HealthBar:SetMinMaxValues(0, UnitHealthMax("party3")); FBHealBox4.HealthBar:SetValue(UnitHealth("party3")); if (HPPercent > LowHP) then FBHealBox4.HealthBar:SetStatusBarColor(0,1,0,1); end; if (HPPercent < LowHP) then FBHealBox4.HealthBar:SetStatusBarColor(1,0.9,0,1); end; if (HPPercent < VeryLowHP) then FBHealBox4.HealthBar:SetStatusBarColor(1,0,0,1); end; end if ((event == "UNIT_HEALTH" ) and (arg1 == "party4")) then HPPercent = UnitHealth("party4") / UnitHealthMax("party4"); FBHealBox5.HPText:SetText(format("%d%%", HPPercent*100)); FBHealBox5.HealthBar:SetMinMaxValues(0, UnitHealthMax("party4")); FBHealBox5.HealthBar:SetValue(UnitHealth("party4")); if (HPPercent > LowHP) then FBHealBox5.HealthBar:SetStatusBarColor(0,1,0,1); end; if (HPPercent < LowHP) then FBHealBox5.HealthBar:SetStatusBarColor(1,0.9,0,1); end; if (HPPercent < VeryLowHP) then FBHealBox5.HealthBar:SetStatusBarColor(1,0,0,1); end; end if event == "UNIT_HEALTH" or event == "UNIT_AURA" then if arg1 == "player" then FBHealBox_UpdateUnit("player", FBHealBox1) end if arg1 == "party1" then FBHealBox_UpdateUnit("party1", FBHealBox2) end if arg1 == "party2" then FBHealBox_UpdateUnit("party2", FBHealBox3) end if arg1 == "party3" then FBHealBox_UpdateUnit("party3", FBHealBox4) end if arg1 == "party4" then FBHealBox_UpdateUnit("party4", FBHealBox5) end end end -- Alle drei Balken in dieselbe Ebene legen und stabil stapeln: -- HP deckend oben, darunter der Schild-Anteil, ganz unten die Heilvorhersage. function FBHealBox_SetBarStrata(f, strata) if (not f) or (not f.HealthBar) then return; end f.IncHealBar:SetFrameStrata(strata); f.ShieldBar:SetFrameStrata(strata); f.HealthBar:SetFrameStrata(strata); f.IncHealBar:SetFrameLevel(1); f.ShieldBar:SetFrameLevel(2); f.HealthBar:SetFrameLevel(3); end function FBHealBoxCreateFrame(FrameName,ParentFrame,FrameTexture,FrameWidth,FrameHeight,FrameAlpha,Unit) local f = CreateFrame("Frame", FrameName, ParentFrame); f:SetFrameStrata("MEDIUM"); icon = f:CreateTexture(nil, "BACKGROUND"); icon:SetAllPoints(); f:SetBackdrop({bgFile = nil, edgeFile = "Interface/Tooltips/UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 10, insets = { left = 4, right = 4, top = 4, bottom = 4 }}); f:SetAlpha(FrameAlpha); f:SetHeight(FrameHeight); f:SetWidth(FrameWidth); f.NameText = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); f.NameText:SetPoint("TOPLEFT", f, "TOPRIGHT", -120, -8); f.NameText:SetText(""); f.NameText:SetTextColor(1, 1, 1, 1); f.NameText:SetWidth(NamePlateWidth - 40); f.HPText = f:CreateFontString(nil, "OVERLAY", "NumberFontNormalYellow"); f.HPText:SetPoint("RIGHT", -5, 0); f.HPText:SetText("100%"); f.HPText:SetTextColor(1, 1, 1, 1); f.HealthBar = CreateFrame("STATUSBAR", nil, f, "TextStatusBar"); f.HealthBar:SetWidth(NamePlateWidth - 5); f.HealthBar:SetHeight(NamePlateHeight - 5); f.HealthBar:SetPoint("TOPLEFT", 2, -3); f.HealthBar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar"); f.HealthBar:SetMinMaxValues(0, UnitHealthMax(Unit)); f.HealthBar:SetValue(UnitHealth(Unit)); f.HealthBar:SetStatusBarColor(0, 1, 0, 1); f.HealthBar:Show(); -- Absorb-Schild als halbtransparentes "Pseudoleben" hinter dem HP-Balken f.ShieldBar = CreateFrame("STATUSBAR", nil, f, "TextStatusBar"); f.ShieldBar:SetWidth(NamePlateWidth - 5); f.ShieldBar:SetHeight(NamePlateHeight - 5); f.ShieldBar:SetPoint("TOPLEFT", 2, -3); f.ShieldBar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar"); f.ShieldBar:SetMinMaxValues(0, UnitHealthMax(Unit)); f.ShieldBar:SetValue(UnitHealth(Unit)); f.ShieldBar:SetStatusBarColor(0.6, 0.8, 1.0, 0.5); f.ShieldBar:Show(); -- Eingehende Heilung (Direktheilung + HoT-Restticks) f.IncHealBar = CreateFrame("STATUSBAR", nil, f, "TextStatusBar"); f.IncHealBar:SetWidth(NamePlateWidth - 5); f.IncHealBar:SetHeight(NamePlateHeight - 5); f.IncHealBar:SetPoint("TOPLEFT", 2, -3); f.IncHealBar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar"); f.IncHealBar:SetMinMaxValues(0, UnitHealthMax(Unit)); f.IncHealBar:SetValue(UnitHealth(Unit)); f.IncHealBar:SetStatusBarColor(0.4, 1, 0.4, 0.5); f.IncHealBar:Show(); -- Reihenfolge: HP oben (deckend), darunter Schild, darunter Heilvorhersage FBHealBox_SetBarStrata(f, "LOW"); f:SetMovable(true); f:EnableMouse(true); f:SetScript("OnMouseDown", function() FBHealBox1:StartMoving() end); f:SetScript("OnMouseUp", function() FBHealBox1:StopMovingOrSizing() end); return f; end function FBHealBoxCreateButton(FBButtonName, FBParentFrame, xoffset, yoffset, texture, tooltiptext, targetUnit, spellID) if (FBButtonName == "") then FBButtonName = "Button"..random(10); end local button = CreateFrame("Button", FBButtonName, FBParentFrame); button:SetPoint("LEFT", FBParentFrame, "RIGHT", xoffset, yoffset); button.icon = button:CreateTexture("icon", "BACKGROUND"); button.icon:SetAllPoints(); local highlight = button:CreateTexture(nil, "HIGHLIGHT"); highlight:SetAllPoints(); highlight:SetBlendMode("ADD"); highlight:SetTexture("Interface/Buttons/ButtonHilight-Square"); button:SetPushedTexture("Interface/Buttons/UI-Quickslot-Depress"); if (texture) then button.icon:SetTexture(texture); else button.icon:SetTexture("Interface/Icons/INV_Misc_QuestionMark"); end button:EnableMouse(1); button:SetHeight(28); button:SetWidth(28); button.TargetUnit = targetUnit; button.spellName = tooltiptext; button.id = spellID; button:SetScript("OnEnter", function() GameTooltip:SetOwner(button, "ANCHOR_RIGHT", -30, 5); if (tooltiptext == nil or button.id == nil) then GameTooltip:SetText(FBT("TT_NO_SPELL")); else GameTooltip_SetDefaultAnchor(GameTooltip, this); GameTooltip:SetSpell(this.id, SpellBookFrame.bookType); GameTooltip:AddLine(FBADDON_NAME.." "..FBT("TT_TARGET")..": |cFF00FF00"..UnitName(this.TargetUnit), 1, 1, 1); end GameTooltip:Show(); end); button:SetScript("OnLeave", function() GameTooltip:Hide(); end); button:RegisterForClicks("LeftButtonUp"); button:SetScript("OnClick", function() if (not button.spellName) then return; end local castTarget = button.TargetUnit or "player"; if (castTarget == "player") then -- Ziel VOR dem Cast merken: SPELLCAST_START feuert sofort FBPredict_NoteCast(button.spellName, UnitName("player")); CastSpellByName(button.spellName, 1); return; end if (not UnitExists(castTarget)) then DEFAULT_CHAT_FRAME:AddMessage("|cFFFF0000"..FBADDON_NAME..":|r "..castTarget..FBT("NOT_IN_GROUP")); return; end if (SUPERWOW_VERSION or SUPERWOW_STRING) then FBPredict_NoteCast(button.spellName, UnitName(castTarget)); CastSpellByName(button.spellName, castTarget); else local hadTarget = UnitExists("target"); local targetWasSame = false; if (hadTarget and UnitIsUnit) then targetWasSame = UnitIsUnit("target", castTarget); end if (not targetWasSame) then TargetUnit(castTarget); end FBPredict_NoteCast(button.spellName, UnitName(castTarget)); CastSpellByName(button.spellName); if (not targetWasSame) then if (hadTarget) then TargetLastTarget(); else ClearTarget(); end end end end); button:RegisterEvent("SPELL_UPDATE_USABLE"); button:SetScript("OnEvent", HealBoxButton_OnEvent); return button; end function FBHealBoxSetup() FBHealBox1=FBHealBoxCreateFrame("FBHealBox1",UIParent,"Interface/DialogFrame/UI-DialogBox-Background",NamePlateWidth,NamePlateHeight,1,"player") FBHealBox2=FBHealBoxCreateFrame("FBHealBox2",FBHealBox1,"Interface/DialogFrame/UI-DialogBox-Background",NamePlateWidth,NamePlateHeight,1,"party1") FBHealBox3=FBHealBoxCreateFrame("FBHealBox3",FBHealBox1,"Interface/DialogFrame/UI-DialogBox-Background",NamePlateWidth,NamePlateHeight,1,"party2") FBHealBox4=FBHealBoxCreateFrame("FBHealBox4",FBHealBox1,"Interface/DialogFrame/UI-DialogBox-Background",NamePlateWidth,NamePlateHeight,1,"party3") FBHealBox5=FBHealBoxCreateFrame("FBHealBox5",FBHealBox1,"Interface/DialogFrame/UI-DialogBox-Background",NamePlateWidth,NamePlateHeight,1,"party4") FBPartyFrame = { FBHealBox1, FBHealBox2, FBHealBox3, FBHealBox4, FBHealBox5 }; FBPartyTable = { FBParty1, FBParty2, FBParty3, FBParty4, FBParty5 }; HealBoxAttachMode(HealBox.AttachMode); end function FBHealBox_RefreshAllBars() if (not FBHealBox1) or (not FBHealBox1.ShieldBar) then return; end if (UnitExists("player")) then FBHealBox_UpdateUnit("player", FBHealBox1); end if (UnitExists("party1")) then FBHealBox_UpdateUnit("party1", FBHealBox2); end if (UnitExists("party2")) then FBHealBox_UpdateUnit("party2", FBHealBox3); end if (UnitExists("party3")) then FBHealBox_UpdateUnit("party3", FBHealBox4); end if (UnitExists("party4")) then FBHealBox_UpdateUnit("party4", FBHealBox5); end end function FBUpdateNames() FBHealBox1:Hide(); FBHealBox2:Hide(); FBHealBox3:Hide(); FBHealBox4:Hide(); FBHealBox5:Hide(); FBPlayerName = strupper(GetUnitName("player")); if (HealBox.Active == 1) then FBHealBox1:Show(); end if(UnitExists("party1")) then FBParty1Name = strupper(GetUnitName("party1")); FBHealBox2:Show(); end if(UnitExists("party2")) then FBParty2Name = strupper(GetUnitName("party2")); FBHealBox3.HealthBar:SetMinMaxValues(0,UnitHealth("party2")); FBHealBox3:Show(); end if(UnitExists("party3")) then FBParty3Name = strupper(GetUnitName("party3")); FBHealBox4.HealthBar:SetMinMaxValues(0,UnitHealth("party3")); FBHealBox4:Show(); end if(UnitExists("party4")) then FBParty4Name = strupper(GetUnitName("party4")); FBHealBox5.HealthBar:SetMinMaxValues(0,UnitHealth("party4")); FBHealBox5:Show(); end FBHealBox1.NameText:SetText(FBPlayerName); FBHealBox2.NameText:SetText(FBParty1Name); FBHealBox3.NameText:SetText(FBParty2Name); FBHealBox4.NameText:SetText(FBParty3Name); FBHealBox5.NameText:SetText(FBParty4Name); end -- Beschriftet die bereits gebaute Oberflaeche neu. Wird beim Bauen des -- Optionsfensters, beim Laden der gespeicherten Sprache und bei jedem -- Sprachwechsel aufgerufen - ein /reload ist nie noetig. function FBHealBox_ApplyLocale() if (MMButton) then MMButton.tooltipText = FBT("MM_TIP"); end if (not panel) then return; end panel.TitleText:SetText(FBADDON_NAME .. " " .. HealBoxVersion); panel.TitleSubText:SetText(format(FBT("PANEL_SUB"), FBADDON_NAME)); panel.AboutText:SetText(format(FBT("ABOUT"), FBADDON_NAME, HealBoxVersion)); for i = 1, MaxButtonCount, 1 do local b = FBSpellBtns[i]; if (b) then if (b.label) then b.label:SetText(FBT("BUTTON") .. " " .. i); end -- belegte Buttons zeigen den Zaubernamen, der bleibt wie er ist if (not FBDropDownButton[i]) then b.text:SetText(FBT("SELECT_SPELL")); end end end FBUpdateButtonSliderText(); FBUpdateScaleSliderText(); if (ScaleSlider) then getglobal(ScaleSlider:GetName() .. "Low"):SetText(FBT("SMALL")); getglobal(ScaleSlider:GetName() .. "High"):SetText(FBT("LARGE")); end if (AttachModeCheck) then AttachModeCheck.Text:SetText(FBT("ATTACH")); AttachModeCheck.tooltipText = FBT("ATTACH_TIP"); end if (HealCommCheck) then HealCommCheck.Text:SetText(FBT("COMM")); HealCommCheck.tooltipText = FBT("COMM_TIP"); end if (FBLangBtn) then FBLangBtn.text:SetText(FBT("LANGUAGE") .. ": |cFFFFFFFF" .. FBT("LANG_NAME")); end end -- [ Options-Fenster ] -- function FBHealBoxCreateAddonOptionFrame() panel = CreateFrame("FRAME", "HealBoxOptionsFrame", UIParent); panel.name = FBADDON_NAME; panel:SetWidth(460); 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:SetMovable(true); panel:EnableMouse(true); panel:RegisterForDrag("LeftButton"); panel:SetScript("OnDragStart", function() panel:StartMoving(); end); panel:SetScript("OnDragStop", function() panel:StopMovingOrSizing(); end); panel:Hide(); panel.CloseButton = CreateFrame("Button", "HealBoxOptionsFrameClose", panel, "UIPanelCloseButton"); panel.CloseButton:SetPoint("TOPRIGHT", -5, -5); panel.CloseButton:SetScript("OnClick", function() FBMenu_CloseAll(); panel:Hide(); end); panel.TitleText = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge"); panel.TitleText:SetPoint("TOPLEFT", 25, -18); panel.TitleText:SetText(FBADDON_NAME .. " " .. HealBoxVersion); panel.TitleSubText = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); panel.TitleSubText:SetPoint("TOPLEFT", 25, -38); panel.TitleSubText:SetJustifyH("LEFT"); panel.TitleSubText:SetText(format(FBT("PANEL_SUB"), FBADDON_NAME)); panel.TitleSubText:SetTextColor(1, 1, 1, 1); local classIcon = CreateFrame("Frame", nil, panel); classIcon:SetPoint("TOPRIGHT", -25, -20); classIcon:SetWidth(70); classIcon:SetHeight(70); classIcon.tex = classIcon:CreateTexture(nil, "BACKGROUND"); classIcon.tex:SetAllPoints(); classIcon.tex:SetTexture(ClassIcon[FBClass]); classIcon.text = classIcon:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge"); classIcon.text:SetText(strupper(FBClass)); classIcon.text:SetPoint("CENTER", 0, -45); classIcon.text:SetTextColor(1, 1, 0.2, 1); -- 10 Buttons für Zauber mit Rank-Auswahl for i = 1, MaxButtonCount do local col = (i > 5) and 1 or 0; local row = (i > 5) and (i - 5) or i; local xPos = 35 + (col * 210); local yPos = -130 - ((row - 1) * 58); local btn = CreateFrame("Button", "FBHealBoxBtn"..i, panel); btn:SetWidth(180); btn:SetHeight(28); btn:SetPoint("TOPLEFT", panel, "TOPLEFT", xPos, yPos); btn: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 } }); btn:SetBackdropColor(0, 0, 0, 0.8); btn:SetBackdropBorderColor(0.6, 0.6, 0.6, 1); btn.icon = btn:CreateTexture(nil, "ARTWORK"); btn.icon:SetWidth(20); btn.icon:SetHeight(20); btn.icon:SetPoint("LEFT", 4, 0); btn.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark"); btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); btn.text:SetPoint("LEFT", btn.icon, "RIGHT", 8, 0); btn.text:SetPoint("RIGHT", -8, 0); btn.text:SetJustifyH("LEFT"); btn.text:SetText(FBT("SELECT_SPELL")); btn.label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal"); btn.label:SetPoint("BOTTOMLEFT", btn, "TOPLEFT", 2, 2); btn.label:SetText(FBT("BUTTON") .. " " .. i); local hl = btn:CreateTexture(nil, "HIGHLIGHT"); hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight"); hl:SetBlendMode("ADD"); hl:SetAllPoints(btn); local btnIndex = i; btn:SetScript("OnClick", function() PlaySound("igMainMenuOptionCheckBoxOn"); FBMenu_OpenSpellMenu(btnIndex, btn); end); FBSpellBtns[i] = btn; end MaxButtonSlider = CreateFrame("Slider", "MaxButtonSlider", panel, "OptionsSliderTemplate"); MaxButtonSlider:SetWidth(128); MaxButtonSlider:SetHeight(16); MaxButtonSlider:SetPoint("TOPLEFT", 75, -460); MaxButtonSlider:SetMinMaxValues(0, MaxButtonCount); MaxButtonSlider:SetValueStep(1); MaxButtonSlider:SetValue(HealBox.MaxButtons); MaxButtonSlider.Text = MaxButtonSlider:CreateFontString(nil, "BACKGROUND", "GameFontNormalLarge"); MaxButtonSlider.Text:SetPoint("CENTER", 0, 17); FBUpdateButtonSliderText(); getglobal(MaxButtonSlider:GetName() .. "Low"):SetText("0"); getglobal(MaxButtonSlider:GetName() .. "High"):SetText(tostring(MaxButtonCount)); MaxButtonSlider:SetScript("OnValueChanged", MaxButtonSlider_Update); ScaleSlider = CreateFrame("Slider", "ScaleSlider", panel, "OptionsSliderTemplate"); ScaleSlider:SetWidth(100); ScaleSlider:SetHeight(16); ScaleSlider:SetPoint("TOPLEFT", 260, -460); ScaleSlider:SetMinMaxValues(0.6, 1.5); ScaleSlider:SetValueStep(0.1); ScaleSlider:SetValue(HealBox.Scale); ScaleSlider.Text = ScaleSlider:CreateFontString(nil, "BACKGROUND", "GameFontNormalLarge"); ScaleSlider.Text:SetPoint("CENTER", -5, 17); FBUpdateScaleSliderText(); getglobal(ScaleSlider:GetName() .. "Low"):SetText(FBT("SMALL")); getglobal(ScaleSlider:GetName() .. "High"):SetText(FBT("LARGE")); ScaleSlider:SetScript("OnValueChanged", function() HealBox.Scale = ScaleSlider:GetValue(); HealBoxScale(FBHealBox1, HealBox.Scale); FBUpdateScaleSliderText(); end); AttachModeCheck = CreateFrame("CheckButton", "$parentCheckButton", panel, "OptionsCheckButtonTemplate"); AttachModeCheck:SetPoint("TOPLEFT", 40, -500); AttachModeCheck.Text = AttachModeCheck:CreateFontString(nil, "BACKGROUND", "GameFontNormal"); AttachModeCheck.Text:SetPoint("LEFT", AttachModeCheck, "RIGHT", 4, 1); AttachModeCheck.Text:SetText(FBT("ATTACH")); AttachModeCheck.tooltipText = FBT("ATTACH_TIP"); AttachModeCheck:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT"); GameTooltip:SetText(FBT("ATTACH")); GameTooltip:AddLine(this.tooltipText, 1, 1, 1, true); GameTooltip:Show(); end); AttachModeCheck:SetScript("OnLeave", function() GameTooltip:Hide(); end); AttachModeCheck:SetScript("OnClick", function() HealBox.AttachMode = AttachModeCheck:GetChecked(); HealBoxAttachMode(HealBox.AttachMode); FBUpdateNames(); end); HealCommCheck = CreateFrame("CheckButton", "FBHealBoxHealCommCheck", panel, "OptionsCheckButtonTemplate"); HealCommCheck:SetPoint("TOPLEFT", 250, -500); HealCommCheck:SetChecked(1); HealCommCheck.Text = HealCommCheck:CreateFontString(nil, "BACKGROUND", "GameFontNormal"); HealCommCheck.Text:SetPoint("LEFT", HealCommCheck, "RIGHT", 4, 1); HealCommCheck.Text:SetText(FBT("COMM")); HealCommCheck.tooltipText = FBT("COMM_TIP"); HealCommCheck:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT"); GameTooltip:SetText(FBT("COMM")); GameTooltip:AddLine(this.tooltipText, 1, 1, 1, true); GameTooltip:Show(); end); HealCommCheck:SetScript("OnLeave", function() GameTooltip:Hide(); end); HealCommCheck:SetScript("OnClick", function() HealBox.HealComm = HealCommCheck:GetChecked() and 1 or 0; if (HealBox.HealComm == 0) then FBCommHeals = {}; FBHealBox_RefreshAllBars(); end end); -- Sprachumschalter: nutzt dasselbe Kaskadenmenue wie die Zauberwahl FBLangBtn = CreateFrame("Button", "FBHealBoxLangBtn", panel); FBLangBtn:SetWidth(180); FBLangBtn:SetHeight(26); FBLangBtn:SetPoint("TOPLEFT", panel, "TOPLEFT", 35, -535); FBLangBtn: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 } }); FBLangBtn:SetBackdropColor(0, 0, 0, 0.8); FBLangBtn:SetBackdropBorderColor(0.6, 0.6, 0.6, 1); FBLangBtn.text = FBLangBtn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); FBLangBtn.text:SetPoint("LEFT", 10, 0); FBLangBtn.text:SetPoint("RIGHT", -10, 0); FBLangBtn.text:SetJustifyH("LEFT"); local langHl = FBLangBtn:CreateTexture(nil, "HIGHLIGHT"); langHl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight"); langHl:SetBlendMode("ADD"); langHl:SetAllPoints(FBLangBtn); FBLangBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT"); GameTooltip:SetText(FBT("LANGUAGE")); GameTooltip:AddLine(FBT("LANG_TIP"), 1, 1, 1, true); GameTooltip:Show(); end); FBLangBtn:SetScript("OnLeave", function() GameTooltip:Hide(); end); FBLangBtn:SetScript("OnClick", function() PlaySound("igMainMenuOptionCheckBoxOn"); FBMenu_OpenLanguageMenu(FBLangBtn); end); panel.AboutText = panel:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall"); panel.AboutText:SetPoint("BOTTOM", panel, "BOTTOM", 0, 14); panel.AboutText:SetWidth(400); panel.AboutText:SetJustifyH("CENTER"); panel.AboutText:SetText(format(FBT("ABOUT"), FBADDON_NAME, HealBoxVersion)); FBHealBox_ApplyLocale(); end -- [ Namensplaketten Buttons ] -- FBPartyUnit = { "player", "party1", "party2", "party3", "party4" }; FBPartyFrame = {}; FBPartyTable = {}; function FBHealBoxButtons() for p=1, 5, 1 do for i=1, MaxButtonCount, 1 do if (FBPartyTable[p][i]) then FBPartyTable[p][i]:Hide(); end; end end for p=1, 5, 1 do local unit = FBPartyUnit[p]; local parentFrame = FBPartyFrame[p]; local prevButton = nil; for i=1, MaxButtonCount, 1 do local anchor = prevButton or parentFrame; FBPartyTable[p][i] = FBHealBoxCreateButton("", anchor, xSpacing, 0, FBDropDownButtonIcon[i], FBDropDownButton[i], unit, FBActiveSpellIDs[i]); FBPartyTable[p][i].TargetUnit = unit; FBPartyTable[p][i].spellName = FBDropDownButton[i]; prevButton = FBPartyTable[p][i]; end end for p=1, 5, 1 do for i=1, MaxButtonCount, 1 do FBPartyTable[p][i]:Hide(); end end for p=1, 5, 1 do for i=1, HealBox.MaxButtons, 1 do FBPartyTable[p][i]:Show(); end end end function FBHealBoxButtonsChanged() for p=1, 5, 1 do for i=1, MaxButtonCount, 1 do if (FBPartyTable[p][i]) then FBPartyTable[p][i]:Hide(); end; end end for p=1, 5, 1 do local unit = FBPartyUnit[p]; for i=1, MaxButtonCount, 1 do if (FBPartyTable[p][i]) then FBPartyTable[p][i].TargetUnit = unit; FBPartyTable[p][i].spellName = FBDropDownButton[i]; FBPartyTable[p][i].id = FBActiveSpellIDs[i]; if FBDropDownButtonIcon[i] then FBPartyTable[p][i].icon:SetTexture(FBDropDownButtonIcon[i]); else FBPartyTable[p][i].icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark"); end end end end for p=1, 5, 1 do for i=1, HealBox.MaxButtons, 1 do if (FBPartyTable[p][i]) then FBPartyTable[p][i]:Show(); end; end end end -- Sliderbeschriftungen: eigene Funktionen, damit der Sprachwechsel sie neu setzen kann function FBUpdateButtonSliderText() if (MaxButtonSlider and MaxButtonSlider.Text) then MaxButtonSlider.Text:SetText(format(FBT("SHOW_BUTTONS"), MaxButtonSlider:GetValue())); end end function FBUpdateScaleSliderText() if (ScaleSlider and ScaleSlider.Text) then ScaleSlider.Text:SetText(format(FBT("SCALE"), format("%.1f", ScaleSlider:GetValue()))); end end function MaxButtonSlider_Update() FBUpdateButtonSliderText(); HealBox.MaxButtons = MaxButtonSlider:GetValue(); FBHealBoxButtonsChanged(); end function CreateMiniMapButton() local button = CreateFrame("Button", "FBHealMiniMap", Minimap); button:SetFrameStrata("MEDIUM"); button:SetFrameLevel(8); button:SetWidth(31); button:SetHeight(31); button:EnableMouse(1); button:RegisterForClicks("LeftButtonUp", "RightButtonUp"); button:RegisterForDrag("RightButton"); button.icon = button:CreateTexture("FBHealMiniMapIcon", "BACKGROUND"); button.icon:SetWidth(17); button.icon:SetHeight(17); button.icon:SetPoint("TOPLEFT", button, "TOPLEFT", 7, -6); button.icon:SetTexture("Interface/Icons/Spell_Holy_GreaterHeal"); button.icon:SetTexCoord(0.075, 0.925, 0.075, 0.925); button.border = button:CreateTexture("FBHealMiniMapBorder", "OVERLAY"); button.border:SetWidth(53); button.border:SetHeight(53); button.border:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0); button.border:SetTexture("Interface/Minimap/MiniMap-TrackingBorder"); button:SetHighlightTexture("Interface/Minimap/UI-Minimap-ZoomButton-Highlight", "ADD"); button.tooltipTitle = FBADDON_NAME; button.tooltipText = FBT("MM_TIP"); local mmAngle = math.rad(225); button:SetPoint("TOPLEFT", "Minimap", "TOPLEFT", 52-(80*cos(mmAngle)), (80*sin(mmAngle))-52); button:Show(); button:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_LEFT"); GameTooltip:SetText(this.tooltipTitle); GameTooltip:AddLine(this.tooltipText, 1, 1, 1); GameTooltip:Show(); end); button:SetScript("OnLeave", function() GameTooltip:Hide(); end); button:SetMovable(true); button:SetScript("OnMouseDown", function() if (arg1 == "RightButton") then this:StartMoving(); end end); button:SetScript("OnMouseUp", function() if (arg1 == "RightButton") then this:StopMovingOrSizing(); end end); button:SetScript("OnClick", function() if (arg1 == "LeftButton") then if (IsShiftKeyDown()) then HealBox.Active = 1 - HealBox.Active; if (HealBox.Active == 1) then FBHealBox1:Show(); else FBHealBox1:Hide(); end else if (panel:IsVisible()) then FBMenu_CloseAll(); panel:Hide(); else panel:Show(); end end end end); return button; end function FBGetSpellID(spell, rank, debug) local i = 1; local spellID; local highestRank; while true do local spellName = GetSpellName(i, SpellBookFrame.bookType); if (not spellName) then break; end if (spellName == spell) then spellID = i; highestRank = spellRank; end i = i + 1; if (i > 300) then break; end end return spellID, highestRank; end function HealBoxButton_OnEvent(this, event, arg1) if (HealBox.Active == 0) then return 0; end if event=="SPELL_UPDATE_USABLE" then if (this.id) then local isUsable, noMana = IsUsableSpell(this.id, BOOKTYPE_SPELL); icon = getglobal(this:GetName()); if isUsable then this.icon:SetVertexColor(1.0, 1.0, 1.0); elseif noMana then this.icon:SetVertexColor(0.5, 0.5, 1.0); else this.icon:SetVertexColor(0.3, 0.3, 0.3); end local inRange = IsSpellInRange(this.id, BOOKTYPE_SPELL, this.TargetUnit); if (inRange == 0) then this.icon:SetVertexColor(1.0, 0.3, 0.3); end end end end function HealBoxScale(this, scale) this:SetScale(scale); end function HealBoxAttachMode(mode) local xoffset = 119; local yoffset = 6; if (mode == 1) then FBHealBox1:SetScale(1); FBHealBox2:SetPoint("LEFT", PartyMemberFrame1, "LEFT", xoffset, yoffset); FBHealBox2:SetBackdropColor(0,0,0,0.8); FBHealBox2:SetWidth(1); FBHealBox2:SetHeight(1); FBHealBox2.NameText:Hide(); FBHealBox2.HPText:Hide(); FBHealBox2.HealthBar:Hide(); FBHealBox2.ShieldBar:Hide(); FBHealBox2.IncHealBar:Hide(); FBHealBox2:SetFrameStrata("LOW"); FBHealBox2:Hide(); FBHealBox3:SetPoint("LEFT", PartyMemberFrame2, "LEFT", xoffset, yoffset); FBHealBox3:SetBackdropColor(0,0,0,0.8); FBHealBox3:SetWidth(1); FBHealBox3:SetHeight(1); FBHealBox3.NameText:Hide(); FBHealBox3.HPText:Hide(); FBHealBox3.HealthBar:Hide(); FBHealBox3.ShieldBar:Hide(); FBHealBox3.IncHealBar:Hide(); FBHealBox3:SetFrameStrata("LOW"); FBHealBox3:Hide(); FBHealBox4:SetPoint("LEFT", PartyMemberFrame3, "LEFT", xoffset, yoffset); FBHealBox4:SetBackdropColor(0,0,0,0.8); FBHealBox4:SetWidth(1); FBHealBox4:SetHeight(1); FBHealBox4.NameText:Hide(); FBHealBox4.HPText:Hide(); FBHealBox4.HealthBar:Hide(); FBHealBox4.ShieldBar:Hide(); FBHealBox4.IncHealBar:Hide(); FBHealBox4:SetFrameStrata("LOW"); FBHealBox4:Hide(); FBHealBox5:SetPoint("LEFT", PartyMemberFrame4, "LEFT", xoffset, yoffset); FBHealBox5:SetBackdropColor(0,0,0,0.8); FBHealBox5:SetWidth(1); FBHealBox5:SetHeight(1); FBHealBox5.NameText:Hide(); FBHealBox5.HPText:Hide(); FBHealBox5.HealthBar:Hide(); FBHealBox5.ShieldBar:Hide(); FBHealBox5.IncHealBar:Hide(); FBHealBox5:SetFrameStrata("LOW"); FBHealBox5:Hide(); else FBHealBox1:SetScale(HealBox.Scale); FBHealBox1:SetPoint("LEFT", WorldFrame, "LEFT", 100, 22); FBHealBox1:SetBackdropColor(0,0,0,0.8); FBHealBox1.NameText:Show(); FBHealBox1.HPText:Show(); FBHealBox1.HealthBar:Show(); FBHealBox1.ShieldBar:Show(); FBHealBox1.IncHealBar:Show(); FBHealBox1:Hide(); FBHealBox2:SetPoint("LEFT", FBHealBox1, "LEFT", 0, -32); FBHealBox2:SetBackdropColor(0,0,0,0.8); FBHealBox2:SetWidth(NamePlateWidth); FBHealBox2:SetHeight(NamePlateHeight); FBHealBox2.NameText:Show(); FBHealBox2.HPText:Show(); FBHealBox2.HealthBar:Show(); FBHealBox2.ShieldBar:Show(); FBHealBox2.IncHealBar:Show(); FBHealBox_SetBarStrata(FBHealBox2, "BACKGROUND"); FBHealBox2:Hide(); FBHealBox3:SetPoint("LEFT", FBHealBox2, "LEFT", 0, -32); FBHealBox3:SetBackdropColor(0,0,0,0.8); FBHealBox3:SetWidth(NamePlateWidth); FBHealBox3:SetHeight(NamePlateHeight); FBHealBox3.NameText:Show(); FBHealBox3.HPText:Show(); FBHealBox3.HealthBar:Show(); FBHealBox3.ShieldBar:Show(); FBHealBox3.IncHealBar:Show(); FBHealBox_SetBarStrata(FBHealBox3, "BACKGROUND"); FBHealBox3:Hide(); FBHealBox4:SetPoint("LEFT", FBHealBox3, "LEFT", 0, -32); FBHealBox4:SetBackdropColor(0,0,0,0.8); FBHealBox4:SetWidth(NamePlateWidth); FBHealBox4:SetHeight(NamePlateHeight); FBHealBox4.NameText:Show(); FBHealBox4.HPText:Show(); FBHealBox4.HealthBar:Show(); FBHealBox4.ShieldBar:Show(); FBHealBox4.IncHealBar:Show(); FBHealBox_SetBarStrata(FBHealBox4, "BACKGROUND"); FBHealBox4:Hide(); FBHealBox5:SetPoint("LEFT", FBHealBox4, "LEFT", 0, -32); FBHealBox5:SetBackdropColor(0,0,0,0.8); FBHealBox5:SetWidth(NamePlateWidth); FBHealBox5:SetHeight(NamePlateHeight); FBHealBox5.NameText:Show(); FBHealBox5.HPText:Show(); FBHealBox5.HealthBar:Show(); FBHealBox5.ShieldBar:Show(); FBHealBox5.IncHealBar:Show(); FBHealBox_SetBarStrata(FBHealBox5, "BACKGROUND"); FBHealBox5:Hide(); end end -- ========================================================================== -- [ FB Heal Prediction : Direktheilung + HoT-Ticks + Absorb-Schilde ] -- -- Ersetzt FBHealCommLite komplett. Ein Tooltip-Parser, ein Lernspeicher, -- ein Balken-Update - statt zwei Systemen, die sich gegenseitig ins -- Gehege kommen. -- -- 1) Direktheilung (Flash Heal, Greater Heal, Healing Wave, ...) -- SPELLCAST_START liefert Zaubername UND Castdauer - unabhaengig -- davon, ob der Cast vom HealBox-Button, der Aktionsleiste oder aus -- einem Makro kommt. Kein Hook auf CastSpellByName noetig. -- Instants brauchen keine Vorhersage: die Heilung ist da, bevor ein -- Balken sie anzeigen koennte. -- -- 2) HoT-Vorhersage (Renew, Rejuvenation, Regrowth-Anteil, ...) -- UnitBuff() liefert fuer fremde Einheiten keine Restlaufzeit, also -- fuehren wir selbst Buch: Cast vormerken -> UNIT_AURA bestaetigt die -- Anwendung ueber die Buff-Textur -> Restticks aus -- (Ablauf - GetTime()) / Intervall. Faellt der Buff weg, ist die -- Anzeige sofort weg. -- -- 3) Schild-Vorhersage (Power Word: Shield, ...) -- Maximaler Absorb aus dem Tooltip, Verbrauch aus dem Combatlog -- ("(123 absorbed)"). -- -- Selbstkorrektur: Tooltips liefern in 1.12 nur Basiswerte ohne -- +Heilung. Der Combatlog liefert die Wahrheit - jeder beobachtete -- Tick, jede angekommene Direktheilung und jeder Absorb ueber dem -- Tooltip-Wert schaerft die Schaetzung nach und wird pro Zauber+Rang -- in den SavedVariables gemerkt. Crits werden dabei ignoriert (sie -- wuerden die Vorhersage dauerhaft aufblasen). -- -- Debug: /fbp Status + ausgelesene Tooltip-Werte -- /fbp debug Live-Ausgabe an/aus -- /fbp reset Gelernte Werte verwerfen -- ========================================================================== FBPREDICT_TICK_DEFAULT = 3; -- Standard-Tickintervall in Sekunden FBPREDICT_THROTTLE = 0.2; -- Update-Rate der Vorhersage FBPREDICT_CONFIRM_TIME = 3.0; -- Wartezeit auf Aura-Bestaetigung nach Cast FBPREDICT_TARGET_TIME = 2.0; -- wie lange ein gemerktes Cast-Ziel gilt FBPredictDebug = false; -- Zauber mit abweichendem Tickintervall FBPredictTickInterval = { ["Lifebloom"] = 1, }; FBHoTs = {}; -- [Name] = { [Zauber] = {rank, perTick, interval, expires} } FBShields = {}; -- [Name] = { spell, rank, max, absorbed, expires } FBPredictDirect = nil; -- laufender Direktcast { target, spell, rank, amount, finish } FBPredictWatch = {}; -- [Zauber] = { tex, bookID, rank, hasDirect, hasHoT, hasShield } FBPredictInfo = {}; -- Cache: [bookID] = Tooltip-Auswertung FBPredictPending = nil; -- eigener Cast, wartet auf Aura-Bestaetigung FBPredictCastTarget = nil; -- exaktes Ziel des letzten Button-Casts FBPredictCastTime = 0; FBPredictAccum = 0; FBPredictUnits = { ["player"] = 1, ["party1"] = 1, ["party2"] = 1, ["party3"] = 1, ["party4"] = 1, }; -- [ Tooltip-Scanner ] ------------------------------------------------------ FBPredictTip = CreateFrame("GameTooltip", "FBHealBoxScanTip", nil, "GameTooltipTemplate"); FBPredictTip:SetOwner(UIParent, "ANCHOR_NONE"); function FBPredict_TooltipText(bookID) if (not bookID) then return ""; end FBPredictTip:SetOwner(UIParent, "ANCHOR_NONE"); FBPredictTip:ClearLines(); FBPredictTip:SetSpell(bookID, BOOKTYPE_SPELL); local txt = ""; local i = 1; while (i <= 30) do local fs = getglobal("FBHealBoxScanTipTextLeft"..i); if (not fs) then break; end local line = fs:GetText(); if (line) then txt = txt.." "..line; end i = i + 1; end return txt; end function FBPredict_Find1(txt, patterns) for _, p in ipairs(patterns) do local _, _, a = string.find(txt, p); if (a) then return a; end end return nil; end function FBPredict_Find2(txt, patterns) for _, p in ipairs(patterns) do local _, _, a, b = string.find(txt, p); if (a and b) then return a, b; end end return nil; end -- Wertet den Zauberbuch-Tooltip aus. Ein Zauber kann mehrere Anteile -- haben (Regrowth: Sofortheilung UND HoT), deshalb keine Entweder-Oder- -- Klassifizierung, sondern drei unabhaengige Felder. function FBPredict_GetSpellInfo(bookID, spellName) if (not bookID) then return nil; end if (FBPredictInfo[bookID] ~= nil) then if (FBPredictInfo[bookID] == false) then return nil; end return FBPredictInfo[bookID]; end local txt = FBPredict_TooltipText(bookID); local info = { direct = nil, hot = nil, shield = nil }; local found = false; -- --- Absorb-Schild --- local absorb = FBPredict_Find1(txt, { "absorbing%s+(%d+)", "absorbs%s+(%d+)%s+damage", }); if (absorb) then -- "Lasts 30 sec" bevorzugen; sonst die groesste Sekundenangabe, -- damit nicht die Zauberzeit ("1.5 sec cast") erwischt wird. local dur = FBPredict_Find1(txt, { "[Ll]asts%s+(%d+)%s+sec" }); if (not dur) then local best = 0; for n in string.gfind(txt, "(%d+)%s+sec") do if (tonumber(n) > best) then best = tonumber(n); end end if (best > 0) then dur = best; end end info.shield = { amount = tonumber(absorb), duration = tonumber(dur) or 30 }; found = true; end -- --- Heilung ueber Zeit --- local total, dur = FBPredict_Find2(txt, { "(%d+)%s+damage%s+over%s+(%d+)%s+sec", "(%d+)%s+over%s+(%d+)%s+sec", "for%s+(%d+)[^%d]-over%s+(%d+)%s+sec", }); if (not total) then -- Notfall: letzte Zahl vor "over X sec" nehmen local s, _, d = string.find(txt, "over%s+(%d+)%s+sec"); if (s) then local head = string.sub(txt, 1, s); local last = nil; for n in string.gfind(head, "(%d+)") do last = n; end if (last) then total = last; dur = d; end end end if (total and dur) then local interval = FBPredictTickInterval[spellName] or FBPREDICT_TICK_DEFAULT; local ticks = math.floor((tonumber(dur) / interval) + 0.5); if (ticks < 1) then ticks = 1; end info.hot = { total = tonumber(total), duration = tonumber(dur), interval = interval, ticks = ticks, perTick = tonumber(total) / ticks, }; found = true; end -- --- Sofortheilung --- -- "for 887 to 1033" -> Mittelwert local lo, hi = FBPredict_Find2(txt, { "(%d+)%s+to%s+(%d+)" }); if (lo and hi) then info.direct = (tonumber(lo) + tonumber(hi)) / 2; found = true; else -- Einzelwert, aber nicht den HoT-Betrag doppelt zaehlen local one = FBPredict_Find1(txt, { "for%s+(%d+)" }); if (one) and ((not info.hot) or (tonumber(one) ~= info.hot.total)) then info.direct = tonumber(one); found = true; end end if (found) then FBPredictInfo[bookID] = info; return info; end FBPredictInfo[bookID] = false; return nil; end -- Nach jedem Zauberbuch-Scan: was koennen die gelernten Zauber ueberhaupt? function FBPredict_BuildWatch() FBPredictWatch = {}; FBPredictInfo = {}; for spellName, ranks in pairs(FBPlayerSpells) do local top = ranks[table.getn(ranks)]; local info = FBPredict_GetSpellInfo(top.id, spellName); if (info) then FBPredictWatch[spellName] = { tex = strupper(top.icon or ""), bookID = top.id, rank = top.rank, hasDirect = (info.direct ~= nil), hasHoT = (info.hot ~= nil), hasShield = (info.shield ~= nil), }; end end end -- [ Lernspeicher (wandert in die SavedVariables) ] -------------------------- function FBPredict_MemKey(kind, spell, rank) return kind.."|"..spell.."|"..(rank or ""); end function FBPredict_Remembered(kind, spell, rank) if (not HealBox) or (not HealBox.PredictMemory) then return nil; end return HealBox.PredictMemory[FBPredict_MemKey(kind, spell, rank)]; end function FBPredict_Remember(kind, spell, rank, value) if (not HealBox) then return; end if (not HealBox.PredictMemory) then HealBox.PredictMemory = {}; end HealBox.PredictMemory[FBPredict_MemKey(kind, spell, rank)] = value; end -- [ Tracking starten ] ----------------------------------------------------- function FBPredict_FindBookID(spellName, rank) local ranks = FBPlayerSpells[spellName]; if (not ranks) then return nil; end for _, sd in ipairs(ranks) do if (sd.rank == rank) then return sd.id; end end return ranks[table.getn(ranks)].id; end -- rankKnown = true nur, wenn der Rang aus einem eigenen Button-Cast stammt. -- Nur dann darf der beobachtete Wert dauerhaft gemerkt werden - sonst -- wuerde ein Rang-3-Cast von der Aktionsleiste die Schaetzung fuer den -- Maximalrang verderben. function FBPredict_StartHoT(unitName, spellName, rank, bookID, rankKnown) local info = FBPredict_GetSpellInfo(bookID, spellName); if (not info) or (not info.hot) then return false; end local perTick = FBPredict_Remembered("tick", spellName, rank) or info.hot.perTick; if (not FBHoTs[unitName]) then FBHoTs[unitName] = {}; end FBHoTs[unitName][spellName] = { rank = rank, rankKnown = rankKnown, perTick = perTick, interval = info.hot.interval, expires = GetTime() + info.hot.duration, }; if (FBPredictDebug) then DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[FBP]|r "..format(FBT("DBG_HOT"), spellName, unitName, math.floor(perTick), info.hot.duration)); end -- Nur gesicherte eigene Casts funken. Ein per Aura entdeckter HoT -- koennte auch von einem anderen Heiler stammen - der erste eigene -- Combatlog-Tick holt das unten nach. if (rankKnown) then FBHoTs[unitName][spellName].commSent = true; FBComm_SendHoT(spellName, unitName, info.hot.duration); end return true; end function FBPredict_StartShield(unitName, spellName, rank, bookID, rankKnown) local info = FBPredict_GetSpellInfo(bookID, spellName); if (not info) or (not info.shield) then return false; end local amount = FBPredict_Remembered("absorb", spellName, rank) or info.shield.amount; FBShields[unitName] = { spell = spellName, rank = rank, rankKnown = rankKnown, max = amount, absorbed = 0, expires = GetTime() + info.shield.duration, }; if (FBPredictDebug) then DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[FBP]|r "..format(FBT("DBG_SHIELD"), spellName, unitName, math.floor(amount), info.shield.duration)); end return true; end -- [ Abfrage durch die Balken ] --------------------------------------------- function FBPredict_TicksLeft(e, now) local left = e.expires - now; if (left <= 0) then return 0; end return math.ceil((left / e.interval) - 0.05); end -- Summe aller noch ausstehenden HoT-Ticks function FBGetHoTHeal(unitName) if (not unitName) then return 0; end local t = FBHoTs[unitName]; if (not t) then return 0; end local now = GetTime(); local sum = 0; for _, e in pairs(t) do local n = FBPredict_TicksLeft(e, now); if (n > 0) then sum = sum + (n * e.perTick); end end return sum; end -- Laufender Direktcast auf diese Einheit function FBGetDirectHeal(unitName) local d = FBPredictDirect; if (not d) or (not unitName) then return 0; end if (d.target ~= unitName) then return 0; end if (GetTime() > d.finish) then return 0; end return d.amount; end -- Verbleibender Absorb function FBGetShield(unitName) if (not unitName) then return 0; end local s = FBShields[unitName]; if (not s) then return 0; end if (GetTime() > s.expires) then return 0; end local rem = s.max - s.absorbed; if (rem < 0) then rem = 0; end return rem; end -- [ Eigenen Cast vormerken (aus dem HealBox-Button) ] ----------------------- function FBPredict_SplitCast(castString) local _, _, base, rank = string.find(castString, "^(.+)%((.+)%)$"); if (base) then return base, rank; end return castString, nil; end -- WICHTIG: direkt VOR CastSpellByName aufrufen - SPELLCAST_START feuert -- sofort und braucht das Ziel bereits. function FBPredict_NoteCast(castString, targetName) if (not castString) or (not targetName) then return; end FBPredictCastTarget = targetName; FBPredictCastTime = GetTime(); local base, rank = FBPredict_SplitCast(castString); if (not FBPredictWatch[base]) then return; end FBPredictPending = { spell = base, rank = rank, target = targetName, bookID = FBPredict_FindBookID(base, rank), t = GetTime(), }; end function FBPredict_ResolveTarget() if (FBPredictCastTarget and (GetTime() - FBPredictCastTime) <= FBPREDICT_TARGET_TIME) then return FBPredictCastTarget; end if (UnitExists("target") and UnitIsFriend("player", "target")) then return UnitName("target"); end return UnitName("player"); end -- [ Direktheilung: SPELLCAST_* ] ------------------------------------------- function FBPredict_CastStart(spellName, castMs) if (not spellName) then return; end local w = FBPredictWatch[spellName]; if (not w) or (not w.hasDirect) then return; end local rank = w.rank; local bookID = w.bookID; local rankKnown = false; if (FBPredictPending and FBPredictPending.spell == spellName) then rank = FBPredictPending.rank or rank; bookID = FBPredictPending.bookID or bookID; rankKnown = true; end local info = FBPredict_GetSpellInfo(bookID, spellName); if (not info) or (not info.direct) then return; end local amount = FBPredict_Remembered("direct", spellName, rank) or info.direct; FBPredictDirect = { target = FBPredict_ResolveTarget(), spell = spellName, rank = rank, rankKnown = rankKnown, amount = amount, finish = GetTime() + ((castMs or 0) / 1000) + 0.3, }; if (FBPredictDebug) then DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[FBP]|r "..format(FBT("DBG_CAST"), spellName, FBPredictDirect.target, math.floor(amount))); end -- an andere Heiler funken (Puppeteer & Co. lesen mit) FBComm_SendHealStart(spellName, FBPredictDirect.target, amount, castMs); FBHealBox_RefreshAllBars(); end function FBPredict_CastEnd() if (FBPredictDirect) then FBPredictDirect = nil; FBHealBox_RefreshAllBars(); end end -- [ Aura-Scan: Anwendung bestaetigen, Wegfall erkennen ] -------------------- function FBPredict_ScanUnit(unit) if (not unit) or (not FBPredictUnits[unit]) then return; end if (not UnitExists(unit)) then return; end local name = UnitName(unit); if (not name) then return; end local textures = {}; local i = 1; while (i <= 32) do local tex = UnitBuff(unit, i); if (not tex) then break; end textures[strupper(tex)] = true; i = i + 1; end local dirty = false; -- 1) Eigener Cast wartet auf Bestaetigung? Der hat Vorrang, damit auch -- ein Refresh (Nachcasten) die Uhr neu stellt. if (FBPredictPending and FBPredictPending.target == name) then local w = FBPredictWatch[FBPredictPending.spell]; if (w and textures[w.tex]) then if (w.hasHoT) then dirty = FBPredict_StartHoT(name, FBPredictPending.spell, FBPredictPending.rank, FBPredictPending.bookID, true) or dirty; end if (w.hasShield) then dirty = FBPredict_StartShield(name, FBPredictPending.spell, FBPredictPending.rank, FBPredictPending.bookID, true) or dirty; end FBPredictPending = nil; end end -- 2) Generischer Abgleich: Buff da / Buff weg for spellName, w in pairs(FBPredictWatch) do local present = textures[w.tex]; if (w.hasHoT) then local tracked = FBHoTs[name] and FBHoTs[name][spellName]; if (present and not tracked) then -- z.B. von der Aktionsleiste: hoechster bekannter Rang, -- der erste Combatlog-Tick korrigiert den Wert ohnehin dirty = FBPredict_StartHoT(name, spellName, w.rank, w.bookID) or dirty; elseif (tracked and not present) then FBHoTs[name][spellName] = nil; dirty = true; end end if (w.hasShield) then local tracked = FBShields[name]; if (present and ((not tracked) or tracked.spell ~= spellName)) then dirty = FBPredict_StartShield(name, spellName, w.rank, w.bookID) or dirty; elseif (tracked and tracked.spell == spellName and not present) then -- Schild gebrochen (vor Ablauf) -> Maximum nach oben lernen if (tracked.rankKnown and GetTime() < tracked.expires and tracked.absorbed > tracked.max) then FBPredict_Remember("absorb", tracked.spell, tracked.rank, tracked.absorbed); end FBShields[name] = nil; dirty = true; end end end if (dirty) then FBHealBox_RefreshAllBars(); end end -- [ Combatlog ] ------------------------------------------------------------ function FBPredict_ToPattern(gs, anchor) if (not gs) then return nil; end local p = string.gsub(gs, "([%^%$%(%)%.%[%]%*%+%-%?])", "%%%1"); p = string.gsub(p, "%%s", "(.+)"); p = string.gsub(p, "%%d", "(%%d+)"); if (anchor) then return "^"..p.."$"; end return p; end function FBPredict_InitPatterns() -- "%s gains %d health from your %s." FBPredictPatHoTOther = FBPredict_ToPattern(PERIODICAURAHEALOTHERSELF, true) or "^(.+) gains (%d+) health from your (.+)%.$"; -- "You gain %d health from your %s." FBPredictPatHoTSelf = FBPredict_ToPattern(PERIODICAURAHEALSELFSELF, true) or "^You gain (%d+) health from your (.+)%.$"; -- "Your %s heals %s for %d." (Crits haben eine eigene Formulierung und -- laufen deshalb ins Leere - genau so gewollt) FBPredictPatHealOther = FBPredict_ToPattern(HEALEDSELFOTHER, true) or "^Your (.+) heals (.+) for (%d+)%.$"; -- "Your %s heals you for %d." FBPredictPatHealSelf = FBPredict_ToPattern(HEALEDSELFSELF, true) or "^Your (.+) heals you for (%d+)%.$"; -- " (%d absorbed)" FBPredictPatAbsorb = FBPredict_ToPattern(ABSORB_TRAILER, false) or "%((%d+) absorbed%)"; end -- Events, bei denen das Opfer immer der Spieler selbst ist FBPredictSelfDamage = { ["CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS"] = 1, ["CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"] = 1, ["CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE"] = 1, ["CHAT_MSG_SPELL_DAMAGESHIELDS_ON_SELF"] = 1, }; function FBPredict_ResolveVictim(event, msg) if (FBPredictSelfDamage[event]) then return UnitName("player"); end for name, _ in pairs(FBShields) do if (string.find(msg, name, 1, true)) then return name; end end if (string.find(msg, " you", 1, true) or string.find(msg, "You ", 1, true)) then return UnitName("player"); end return nil; end function FBPredict_OnTick(unitName, amount, spellName) if (not unitName) or (not amount) or (not spellName) then return; end local t = FBHoTs[unitName]; if (not t) or (not t[spellName]) then return; end local e = t[spellName]; -- "... from your Renew" beweist: der HoT ist von uns. Falls er ueber -- die Aktionsleiste kam und noch nicht gefunkt wurde, jetzt nachholen. if (not e.commSent) then e.commSent = true; FBComm_SendHoT(spellName, unitName, e.expires - GetTime()); end if (amount > 0 and e.perTick ~= amount) then e.perTick = amount; if (e.rankKnown) then FBPredict_Remember("tick", spellName, e.rank, amount); end if (FBPredictDebug) then DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00[FBP]|r "..format(FBT("DBG_TICK"), spellName, amount)); end end FBHealBox_RefreshAllBars(); end function FBPredict_OnDirectHeal(spellName, targetName, amount) if (not spellName) or (not amount) then return; end local w = FBPredictWatch[spellName]; if (not w) then return; end -- faengt auch die Crit-Formulierung ab -- Nur lernen, wenn der gecastete Rang gesichert ist (Button-Cast). -- Ein Downrank von der Aktionsleiste wuerde sonst dem Maximalrang -- zugeschrieben und die Vorhersage nach unten ziehen. if (FBPredictDirect and FBPredictDirect.spell == spellName and FBPredictDirect.rankKnown) then local rank = FBPredictDirect.rank; -- Heilungen streuen innerhalb einer Spanne -> sanft einpendeln local old = FBPredict_Remembered("direct", spellName, rank); local value = amount; if (old) then value = (old + amount) / 2; end FBPredict_Remember("direct", spellName, rank, value); if (FBPredictDebug) then DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00[FBP]|r "..format(FBT("DBG_HEAL"), spellName, amount, math.floor(value))); end end if (FBPredictDirect and FBPredictDirect.target == targetName) then FBPredictDirect = nil; end FBHealBox_RefreshAllBars(); end function FBPredict_OnAbsorb(unitName, amount) local s = FBShields[unitName]; if (not s) or (not amount) then return; end s.absorbed = s.absorbed + amount; if (s.absorbed > s.max) then -- mehr absorbiert als der Tooltip hergibt (+Heilung) -> anheben s.max = s.absorbed; if (s.rankKnown) then FBPredict_Remember("absorb", s.spell, s.rank, s.max); end end if (FBPredictDebug) then DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00[FBP]|r "..format(FBT("DBG_ABSORB"), amount, unitName, math.floor(s.max - s.absorbed))); end FBHealBox_RefreshAllBars(); end function FBPredict_ParseCombat(event, msg) if (not msg) then return; end -- Direktheilung auf jemand anderen: "Your Flash Heal heals Bob for 1240." local _, _, spell, who, amt = string.find(msg, FBPredictPatHealOther); if (spell and who and amt) then FBPredict_OnDirectHeal(spell, who, tonumber(amt)); return; end -- Direktheilung auf mich: "Your Flash Heal heals you for 1240." local _, _, spell2, amt2 = string.find(msg, FBPredictPatHealSelf); if (spell2 and amt2) then FBPredict_OnDirectHeal(spell2, UnitName("player"), tonumber(amt2)); return; end -- HoT-Tick auf jemand anderen: "Bob gains 194 health from your Renew." local _, _, who3, amt3, spell3 = string.find(msg, FBPredictPatHoTOther); if (who3 and amt3 and spell3) then FBPredict_OnTick(who3, tonumber(amt3), spell3); return; end -- HoT-Tick auf mich: "You gain 194 health from your Renew." local _, _, amt4, spell4 = string.find(msg, FBPredictPatHoTSelf); if (amt4 and spell4) then FBPredict_OnTick(UnitName("player"), tonumber(amt4), spell4); return; end -- Absorb-Anteil: "... hits Bob for 120. (80 absorbed)" local _, _, abs = string.find(msg, FBPredictPatAbsorb); if (abs) then local victim = FBPredict_ResolveVictim(event, msg); if (victim) then FBPredict_OnAbsorb(victim, tonumber(abs)); end end end -- [ Event-Frame ] ---------------------------------------------------------- FBPredictEvents = { "UNIT_AURA", "CHAT_MSG_ADDON", -- eigener Cast "SPELLCAST_START", "SPELLCAST_STOP", "SPELLCAST_FAILED", "SPELLCAST_INTERRUPTED", "SPELLCAST_DELAYED", -- angekommene Heilung + HoT-Ticks "CHAT_MSG_SPELL_SELF_BUFF", "CHAT_MSG_SPELL_PARTY_BUFF", "CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF", "CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS", "CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS", "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS", "CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS", -- Schaden mit Absorb-Anteil "CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS", "CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS", "CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE", "CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE", "CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", "CHAT_MSG_SPELL_DAMAGESHIELDS_ON_SELF", }; FBPredictFrame = CreateFrame("Frame", "FBHealBoxPredict", UIParent); for _, ev in ipairs(FBPredictEvents) do pcall(function() FBPredictFrame:RegisterEvent(ev); end); end FBPredictFrame:SetScript("OnEvent", function() if (event == "UNIT_AURA") then FBPredict_ScanUnit(arg1); elseif (event == "CHAT_MSG_ADDON") then FBComm_OnMessage(arg1, arg2, arg3, arg4); elseif (event == "SPELLCAST_START") then FBPredict_CastStart(arg1, tonumber(arg2)); elseif (event == "SPELLCAST_STOP") then -- Erfolgreich beendet: HealComm-Empfaenger lassen den Eintrag -- selbst auslaufen, hier wird bewusst kein Healstop gefunkt. FBPredict_CastEnd(); elseif (event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED") then if (FBPredictDirect) then FBComm_SendHealStop(); end FBPredict_CastEnd(); elseif (event == "SPELLCAST_DELAYED") then if (FBPredictDirect and arg1) then FBPredictDirect.finish = FBPredictDirect.finish + (tonumber(arg1) / 1000); FBComm_SendHealDelay(arg1); end else FBPredict_ParseCombat(event, arg1); end end); FBPredictFrame:SetScript("OnUpdate", function() FBPredict_OnUpdate(arg1); end); function FBPredict_OnUpdate(elapsed) FBPredictAccum = FBPredictAccum + (elapsed or 0); if (FBPredictAccum < FBPREDICT_THROTTLE) then return; end FBPredictAccum = 0; local now = GetTime(); local dirty = false; for _, spells in pairs(FBHoTs) do for spellName, e in pairs(spells) do if (now >= e.expires) then spells[spellName] = nil; dirty = true; else local n = FBPredict_TicksLeft(e, now); if (n ~= e.lastTicks) then e.lastTicks = n; dirty = true; end end end end for name, s in pairs(FBShields) do if (now >= s.expires) then FBShields[name] = nil; dirty = true; end end for _, casters in pairs(FBCommHeals) do for caster, info in pairs(casters) do if (now >= info.expires) then casters[caster] = nil; dirty = true; end end end if (FBPredictDirect and now > FBPredictDirect.finish) then FBPredictDirect = nil; dirty = true; end if (FBPredictPending and (now - FBPredictPending.t) > FBPREDICT_CONFIRM_TIME) then FBPredictPending = nil; end if (dirty) then FBHealBox_RefreshAllBars(); end end FBPredict_InitPatterns(); -- ========================================================================== -- [ HealComm-Protokoll ] Interop mit Puppeteer, pfUI, Luna, CT_RaidAssist ... -- -- HealComm-1.0 (Ace2) funkt reinen Klartext ueber SendAddonMessage mit dem -- Prefix "HealComm". Wir sprechen dieselbe Sprache - ohne die Bibliothek -- einzubinden: -- -- Heal//// Direktheilung startet -- Healstop Cast abgebrochen -- Healdelay// Pushback -- GrpHeal/////... Gruppenheilung (Prayer of Healing) -- GrpHealstop / GrpHealdelay// -- Renew|Reju|Regr/// HoT angewendet -- -- Empfaenger ignorieren die eigenen Nachrichten (Absender == man selbst), -- und HealComm legt eingehende Heilungen pro Caster ab - doppelt gesendete -- Nachrichten (z.B. wenn parallel noch ein echtes HealComm laeuft) -- ueberschreiben denselben Eintrag statt sich zu addieren. -- -- Die Betraege sind unsere selbstkorrigierten Werte aus dem Combatlog, -- also inkl. +Heilung und Talenten - kein ItemBonusLib noetig. -- ========================================================================== FBCOMM_PREFIX = "HealComm"; FBCommHeals = {}; -- [Ziel] = { [Caster] = { amount, expires } } FBCommSentGroup = false; -- war der zuletzt gesendete Cast ein Gruppenheal? FBCommHoTCode = { ["Renew"] = "Renew", ["Rejuvenation"] = "Reju", ["Regrowth"] = "Regr", }; FBCommGroupHeal = { ["Prayer of Healing"] = 1, }; function FBComm_Enabled() return (HealBox and HealBox.HealComm and HealBox.HealComm ~= 0); end function FBComm_Send(msg) if (not FBComm_Enabled()) then return; end local n = GetNumRaidMembers(); if (n and n > 0) then SendAddonMessage(FBCOMM_PREFIX, msg, "RAID"); else n = GetNumPartyMembers(); if (n and n > 0) then SendAddonMessage(FBCOMM_PREFIX, msg, "PARTY"); else return; -- allein: niemand da, der zuhoert end end if (FBPredictDebug) then DEFAULT_CHAT_FRAME:AddMessage("|cFF88CCFF[FBP>]|r "..msg); end end -- [ Senden ] --------------------------------------------------------------- function FBComm_SendHealStart(spellName, target, amount, castMs) if (not FBComm_Enabled()) or (not target) then return; end amount = math.floor(amount or 0); castMs = math.floor(castMs or 0); if (amount <= 0) then return; end if (FBCommGroupHeal[spellName]) then -- Gruppenheilung: alle Ziele in Reichweite mitschicken local names = UnitName("player").."/"; for i = 1, 4 do local u = "party"..i; if (UnitExists(u) and CheckInteractDistance(u, 4)) then names = names..UnitName(u).."/"; end end FBCommSentGroup = true; FBComm_Send("GrpHeal/"..amount.."/"..castMs.."/"..names); else FBCommSentGroup = false; FBComm_Send("Heal/"..target.."/"..amount.."/"..castMs.."/"); end end function FBComm_SendHealStop() if (FBCommSentGroup) then FBComm_Send("GrpHealstop"); else FBComm_Send("Healstop"); end end function FBComm_SendHealDelay(ms) ms = tonumber(ms); if (not ms) then return; end if (FBCommSentGroup) then FBComm_Send("GrpHealdelay/"..math.floor(ms).."/"); else FBComm_Send("Healdelay/"..math.floor(ms).."/"); end end function FBComm_SendHoT(spellName, target, duration) local code = FBCommHoTCode[spellName]; if (not code) or (not target) or (not duration) then return; end if (duration < 1) then return; end FBComm_Send(code.."/"..target.."/"..math.floor(duration).."/"); end -- [ Empfangen ] ------------------------------------------------------------ function FBComm_Split(str) local t = {}; for w in string.gfind(str, "([^/]+)") do table.insert(t, w); end return t; end -- Ein Caster hat immer nur einen Heilzauber unterwegs function FBComm_ClearCaster(caster) for _, casters in pairs(FBCommHeals) do if (casters[caster]) then casters[caster] = nil; end end end function FBComm_DelayCaster(caster, ms) local add = (tonumber(ms) or 0) / 1000; for _, casters in pairs(FBCommHeals) do if (casters[caster]) then casters[caster].expires = casters[caster].expires + add; end end end function FBComm_OnMessage(prefix, msg, channel, sender) if (prefix ~= FBCOMM_PREFIX) or (not msg) or (not sender) then return; end if (sender == UnitName("player")) then return; end -- eigene Nachrichten ignorieren if (not FBComm_Enabled()) then return; end local p = FBComm_Split(msg); local cmd = p[1]; local now = GetTime(); local dirty = false; if (cmd == "Heal" and p[2] and p[3] and p[4]) then FBComm_ClearCaster(sender); if (not FBCommHeals[p[2]]) then FBCommHeals[p[2]] = {}; end FBCommHeals[p[2]][sender] = { amount = tonumber(p[3]) or 0, expires = now + ((tonumber(p[4]) or 0) / 1000), }; dirty = true; elseif (cmd == "GrpHeal" and p[2] and p[3]) then FBComm_ClearCaster(sender); local amount = tonumber(p[2]) or 0; local expires = now + ((tonumber(p[3]) or 0) / 1000); local i = 4; while (p[i]) do if (not FBCommHeals[p[i]]) then FBCommHeals[p[i]] = {}; end FBCommHeals[p[i]][sender] = { amount = amount, expires = expires }; i = i + 1; end dirty = true; elseif (cmd == "Healstop" or cmd == "GrpHealstop") then FBComm_ClearCaster(sender); dirty = true; elseif ((cmd == "Healdelay" or cmd == "GrpHealdelay") and p[2]) then FBComm_DelayCaster(sender, p[2]); dirty = true; end -- HoT-Nachrichten (Renew/Reju/Regr) tragen nur Laufzeiten, keine -- Betraege - HealComm selbst zaehlt sie ebenfalls nicht zur -- eingehenden Heilung. Wir ignorieren sie deshalb hier. if (dirty) then FBHealBox_RefreshAllBars(); end end -- Eingehende Heilung anderer Heiler auf diese Einheit function FBGetCommHeal(unitName) if (not unitName) then return 0; end local casters = FBCommHeals[unitName]; if (not casters) then return 0; end local now = GetTime(); local sum = 0; for _, info in pairs(casters) do if (info.expires > now) then sum = sum + info.amount; end end return sum; end -- [ Debug-Kommando ] ------------------------------------------------------- SLASH_FBHEALPREDICT1 = "/fbp"; SlashCmdList["FBHEALPREDICT"] = function(msg) if (msg == "debug") then FBPredictDebug = not FBPredictDebug; DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..FBT("FBP_DEBUG").." "..tostring(FBPredictDebug)); return; end if (msg == "reset") then if (HealBox) then HealBox.PredictMemory = {}; end FBPredictInfo = {}; FBPredict_BuildWatch(); DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..FBT("FBP_RESET")); return; end DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..FBT("FBP_WATCHED")); for spellName, w in pairs(FBPredictWatch) do local info = FBPredict_GetSpellInfo(w.bookID, spellName); if (info) then local parts = ""; if (info.direct) then local learned = FBPredict_Remembered("direct", spellName, w.rank); parts = parts.." "..FBT("FBP_DIRECT").."="..math.floor(info.direct); if (learned) then parts = parts.."("..FBT("FBP_LEARNED").." "..math.floor(learned)..")"; end end if (info.hot) then parts = parts.." HoT="..math.floor(info.hot.perTick).."x"..info.hot.ticks .." "..FBT("FBP_EVERY").." "..info.hot.interval.."s"; end if (info.shield) then parts = parts.." "..FBT("FBP_SHIELD").."="..info.shield.amount .."/"..info.shield.duration.."s"; end DEFAULT_CHAT_FRAME:AddMessage(" "..spellName.." ("..tostring(w.rank)..")"..parts); end end local now = GetTime(); DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..FBT("FBP_ACTIVE")); if (FBPredictDirect) then DEFAULT_CHAT_FRAME:AddMessage(" "..FBT("FBP_CAST").." "..FBPredictDirect.spell .." "..FBT("FBP_ON").." "..FBPredictDirect.target..": " ..math.floor(FBPredictDirect.amount)); end for name, spells in pairs(FBHoTs) do for spellName, e in pairs(spells) do DEFAULT_CHAT_FRAME:AddMessage(" HoT "..name.." / "..spellName..": " ..FBPredict_TicksLeft(e, now).." "..FBT("FBP_TICKSOF").." " ..math.floor(e.perTick)); end end for name, s in pairs(FBShields) do DEFAULT_CHAT_FRAME:AddMessage(" "..FBT("FBP_SHIELD").." "..name.." / "..s.spell ..": "..math.floor(s.max - s.absorbed).." "..FBT("FBP_OF").." " ..math.floor(s.max)); end local state = FBT("FBP_STATE_OFF"); if (FBComm_Enabled()) then state = FBT("FBP_STATE_ON"); end DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..FBT("FBP_SYNC").." "..state); for target, casters in pairs(FBCommHeals) do for caster, info in pairs(casters) do if (info.expires > now) then DEFAULT_CHAT_FRAME:AddMessage(" "..FBT("FBP_INCOMING").." "..caster .." -> "..target..": "..math.floor(info.amount)); end end end end function FBHealBox_UpdateUnit(unit, frame) if not UnitExists(unit) then return end local hp = UnitHealth(unit); local hpMax = UnitHealthMax(unit); local hpPercent = hp / hpMax; frame.HPText:SetText(format("%d%%", hpPercent * 100)); frame.HealthBar:SetMinMaxValues(0, hpMax); frame.HealthBar:SetValue(hp); local name = UnitName(unit); -- Eigener Direktcast + eigene HoT-Restticks + Heilung anderer Heiler local incHeal = FBGetDirectHeal(name) + FBGetHoTHeal(name) + FBGetCommHeal(name); -- Absorb-Schild als Pseudoleben direkt hinter den aktuellen HP local shieldTop = math.min(hpMax, hp + FBGetShield(name)); frame.ShieldBar:SetMinMaxValues(0, hpMax); frame.ShieldBar:SetValue(shieldTop); -- Heilvorhersage haengt hinter dem Schild-Anteil frame.IncHealBar:SetMinMaxValues(0, hpMax); frame.IncHealBar:SetValue(math.min(hpMax, shieldTop + incHeal)); local dispellableTypes = {}; if FBClass == "Priest" then dispellableTypes["Magic"] = {0.2, 0.6, 1, 1}; dispellableTypes["Disease"] = {0.6, 0.4, 0, 1}; elseif FBClass == "Paladin" then dispellableTypes["Magic"] = {0.2, 0.6, 1, 1}; dispellableTypes["Poison"] = {0, 0.6, 0, 1}; dispellableTypes["Disease"] = {0.6, 0.4, 0, 1}; elseif FBClass == "Shaman" then dispellableTypes["Poison"] = {0, 0.6, 0, 1}; dispellableTypes["Disease"] = {0.6, 0.4, 0, 1}; elseif FBClass == "Druid" then dispellableTypes["Curse"] = {0.6, 0, 1, 1}; dispellableTypes["Poison"] = {0, 0.6, 0, 1}; end local hasDebuff = false; for i = 1, 16 do local texture, count, debuffType = UnitDebuff(unit, i); if not texture then break end if debuffType and dispellableTypes[debuffType] then local color = dispellableTypes[debuffType]; frame.HealthBar:SetStatusBarColor(color[1], color[2], color[3], color[4]); hasDebuff = true; break; end end if not hasDebuff then if hpPercent > LowHP then frame.HealthBar:SetStatusBarColor(0, 1, 0, 1); elseif hpPercent > VeryLowHP then frame.HealthBar:SetStatusBarColor(1, 0.9, 0, 1); else frame.HealthBar:SetStatusBarColor(1, 0, 0, 1); end end end MMButton = CreateMiniMapButton(); FBHealBoxSetup(); FBHealBoxCreateAddonOptionFrame();