diff --git a/FBHealBox.lua b/FBHealBox.lua new file mode 100644 index 0000000..52c081e --- /dev/null +++ b/FBHealBox.lua @@ -0,0 +1,2767 @@ +-- ========================================================================== +-- 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(); \ No newline at end of file diff --git a/FBHealBox.toc b/FBHealBox.toc new file mode 100644 index 0000000..0c1b496 --- /dev/null +++ b/FBHealBox.toc @@ -0,0 +1,11 @@ +## Interface: 11200 +## Title: Heal Box Vanilla |cFFFFFF00v1.4|r +## Notes: Party and self heal display with quick-cast buttons for healers. Ported to client 1.21.1 (optional SuperWoW). Up to 10 buttons per row, settings saved per character. Heal prediction for direct heals, remaining HoT ticks and absorb shields, self-correcting from the combat log. HealComm sync with Puppeteer, pfUI and others. German/English, switchable in the options. Original by Dourd, ported to Vanilla and extended 09/2026 by Mquadrat. +## Notes-deDE: Party- und Selbst-Heilanzeige mit Schnellzugriff-Buttons fuer Heiler. Portiert fuer Client 1.21.1 (optional SuperWoW). Bis zu 10 Buttons pro Reihe, Einstellungen pro Charakter. Heal-Prediction fuer Direktheilung, HoT-Restticks und Absorb-Schilde, selbstkorrigierend ueber den Combatlog. HealComm-Sync mit Puppeteer, pfUI und Co. Deutsch/Englisch, in den Optionen umschaltbar. Original von Dourd, Vanilla-Portierung und Erweiterung 09/2026 von Mquadrat. +## Author: Dourd (Argent Dawn EU) | Vanilla-Port & Erweiterung 09/2026: Mquadrat +## Version: 1.4 +## SavedVariablesPerCharacter: HealBox +## OptionalDeps: SuperWoW + +FBHealBox.lua +FBHealBox.xml diff --git a/FBHealBox.xml b/FBHealBox.xml new file mode 100644 index 0000000..e0a50f0 --- /dev/null +++ b/FBHealBox.xml @@ -0,0 +1,19 @@ + + +