diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cc6c14d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,75 @@ +# Changelog + +## 1.4.1 (2026-09-04) + +### English + +**New** + +- **Pets.** Every pet in the group (`pet`, `partypet1` to `partypet4`) gets its own plate with the full set of heal buttons, stacked directly below its owner, indented by 12 px, with a paw icon and a light-blue name to tell it apart. Appears and disappears with `UNIT_PET`. Option *Show pets*. +- **Mana bar.** A 5 px blue bar along the bottom edge of the health bar ("bar in bar"), only for units whose power type is mana. Option *Mana bar*. +- **Button and row spacing.** Two sliders, 0 to 20 px each, for the gap between buttons and the gap between plates. +- **Test mode.** Fills the display with ghost players and pets (health, mana, shield, incoming heal, dispellable debuff, missing buff, out of range) so the layout can be arranged without a group. `/fbp test` or the checkbox. Not saved. +- **Options window with two tabs.** *Buttons* holds the button assignment, the button count and the right-click switch; *General* holds scale, spacing, all other switches, language and buff watch. +- **Right-click spell.** Every button can carry a second spell for right click. Off by default and only enabled through the *Buttons* tab; when on, a second column appears in the assignment and a small corner icon on each button shows the right-click spell. Assignments are kept when the switch is off. +- **Click on a plate.** Left and right click on a name or health bar target the unit by default. Both are configurable (*General* tab): Target, Unit menu, Move display, Nothing. Moving the display is now Shift + left drag (or the *Move display* action). +- **Dead / ghost / offline.** Shown as text in the bar instead of 0 %, with an empty grey bar and no mana strip. +- **Line of sight.** An eye badge on the plate's left edge while the unit is out of line of sight: live via the UnitXP client mod, otherwise inferred from the 'not in line of sight' error after your own heal attempt (8 s, cleared when a cast starts or a heal lands). Option *Line of sight*. +- **Debuff icon.** The icon of the first debuff your class can remove is shown next to the name, with its stack count. Option *Debuff icon*. +- **Class colours.** Names in class colour (client `RAID_CLASS_COLORS`, fallback table built in). Option *Class colours*. +- **Buff watch.** Pick one of your buffs; every plate whose unit is missing it gets an orange border. Group versions count as well, even when cast by another healer (texture check first, tooltip name scan as fallback). Pets are excluded unless *Buff watch on pets* is on. +- **Range fading.** Plates including their buttons fade to 50 % when the unit is out of range of your first assigned spell (28 yards without a spell). Option *Range fading*. +- **`/fbp config`** opens the options window; **Escape** closes it (registered in `UISpecialFrames`, plus a hook on `ToggleGameMenu` for clients that ignore that list). `/fbp` ends with a list of all commands. +- **Saved plate position.** The player plate keeps its position across reloads; scaling no longer moves it. + +**Changed** + +- All UNIT events run through one slot table (`FBPartyUnit`) instead of five copied handlers; `HealBoxAttachMode` and `FBUpdateNames` are loops. +- Spell buttons are created once and only re-assigned afterwards (previously rebuilt on every `SPELLS_CHANGED`, with random global names like `Button3`). +- Settings missing in an old saved-variables table are filled in on load (`FBHealBox_ApplyDefaults`). +- `UNIT_MAXHEALTH` is handled (was missing). + +**Fixed** + +- Documentation named a wrong client version; the addon targets client 1.12.1. +- Tooltip error on a button whose target does not exist. +- Division by zero for offline members (`UnitHealthMax` = 0). +- Plate position reset on every login. + +### Deutsch + +**Neu** + +- **Begleiter.** Jeder Begleiter in der Gruppe (`pet`, `partypet1` bis `partypet4`) bekommt eine eigene Plakette mit allen Heil-Buttons, direkt unter seinem Besitzer, um 12 px eingerueckt, mit Pfoten-Icon und hellblauem Namen zur Unterscheidung. Kommt und geht mit `UNIT_PET`. Option *Begleiter anzeigen*. +- **Manabalken.** 5 px blau am unteren Rand des Lebensbalkens ("Balken im Balken"), nur bei Einheiten mit Powertyp Mana. Option *Manabalken*. +- **Button- und Zeilen-Abstand.** Zwei Regler, je 0 bis 20 px, fuer den Abstand der Buttons und den Abstand der Plaketten. +- **Testmodus.** Fuellt die Anzeige mit Geisterspielern und -begleitern (Leben, Mana, Schild, eingehende Heilung, entfernbarer Debuff, fehlender Buff, ausser Reichweite), damit sich alles ohne Gruppe einrichten laesst. `/fbp test` oder der Haken. Wird nicht gespeichert. +- **Optionsfenster mit zwei Reitern.** *Buttons* enthaelt die Belegung, die Buttonzahl und den Rechtsklick-Schalter; *Allgemein* enthaelt Skalierung, Abstaende, alle uebrigen Schalter, Sprache und Buff-Wache. +- **Rechtsklick-Zauber.** Jeder Button kann einen zweiten Zauber fuer Rechtsklick tragen. Standardmaessig aus und nur ueber den Reiter *Buttons* einschaltbar; eingeschaltet erscheint eine zweite Spalte in der Belegung und ein kleines Eck-Icon auf jedem Button zeigt den Rechtsklick-Zauber. Die Belegung bleibt bei ausgeschaltetem Schalter erhalten. +- **Klick auf die Plakette.** Links- und Rechtsklick auf Name oder Lebensbalken visieren die Einheit an (Standard). Beides belegbar (Reiter *Allgemein*): Anvisieren, Einheitenmenue, Anzeige verschieben, Nichts. Verschieben geht jetzt per Shift + Linksklick ziehen (oder Aktion *Anzeige verschieben*). +- **Tot / Geist / Offline.** Als Text im Balken statt 0 %, mit leerem grauem Balken und ohne Manastreifen. +- **Sichtlinie.** Augen-Abzeichen am linken Plattenrand, solange die Einheit ausserhalb der Sichtlinie ist: live ueber den Client-Mod UnitXP, sonst aus der Fehlermeldung 'nicht in Sichtlinie' nach einem eigenen Heilversuch abgeleitet (8 s, geloescht sobald ein Cast startet oder eine Heilung ankommt). Option *Sichtlinie*. +- **Debuff-Icon.** Das Icon des ersten von deiner Klasse entfernbaren Debuffs erscheint neben dem Namen, mit Stackzahl. Option *Debuff-Icon*. +- **Klassenfarben.** Namen in Klassenfarbe (`RAID_CLASS_COLORS` des Clients, Ersatztabelle eingebaut). Option *Klassenfarben*. +- **Buff-Wache.** Einen eigenen Buff waehlen; jede Plakette, deren Einheit ihn nicht traegt, bekommt einen orangen Rahmen. Gruppenversionen zaehlen mit, auch von anderen Heilern (erst Texturvergleich, dann Tooltip-Namensscan). Begleiter sind ausgenommen, solange *Buff-Wache auch fuer Begleiter* aus ist. +- **Reichweiten-Fading.** Plaketten samt Buttons werden auf 50 % abgeblendet, wenn die Einheit ausser Reichweite des ersten belegten Zaubers ist (ohne Zauber 28 Meter). Option *Reichweiten-Fading*. +- **`/fbp config`** oeffnet das Optionsfenster; **Escape** schliesst es (in `UISpecialFrames` eingetragen, dazu ein Hook auf `ToggleGameMenu` fuer Clients, die diese Liste ignorieren). `/fbp` endet mit einer Liste aller Befehle. +- **Gespeicherte Plattenposition.** Die Spielerplakette behaelt ihre Position ueber Reloads; Skalieren verschiebt sie nicht mehr. + +**Geaendert** + +- Alle UNIT-Events laufen ueber eine Slot-Tabelle (`FBPartyUnit`) statt fuenf kopierter Handler; `HealBoxAttachMode` und `FBUpdateNames` sind Schleifen. +- Zauber-Buttons werden einmal angelegt und danach nur umbelegt (frueher bei jedem `SPELLS_CHANGED` neu gebaut, mit zufaelligen Global-Namen wie `Button3`). +- In einer alten SavedVariables-Tabelle fehlende Einstellungen werden beim Laden nachgezogen (`FBHealBox_ApplyDefaults`). +- `UNIT_MAXHEALTH` wird ausgewertet (fehlte). + +**Behoben** + +- Die Dokumentation nannte eine falsche Client-Version; das Addon zielt auf Client 1.12.1. +- Tooltip-Fehler auf einem Button, dessen Ziel nicht existiert. +- Division durch Null bei Offline-Mitgliedern (`UnitHealthMax` = 0). +- Plattenposition wurde bei jedem Login zurueckgesetzt. + +## 1.4 + +- Vanilla port, cascading spell menu, heal prediction, HealComm sync, German/English localization. See README. diff --git a/FBHealBox.lua b/FBHealBox.lua index 52c081e..7e178b0 100644 --- a/FBHealBox.lua +++ b/FBHealBox.lua @@ -1,14 +1,20 @@ -- ========================================================================== -- 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 +-- Original: "Heal Box" von Dourd (Argent Dawn EU), UI Overhauled. +-- Portierung auf Vanilla, Client 1.12.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 +-- * v1.4.1: Begleiter (Pets) als eigene Plaketten, Manabalken im +-- Lebensbalken, frei waehlbare Button- und Zeilenabstaende, Testmodus +-- mit Geisterspielern, gespeicherte Plattenposition, Klassenfarben, +-- Buff-Wache (oranger Rahmen bei fehlendem Buff), Reichweiten-Fading, +-- Optionsfenster mit Tabs, Rechtsklick-Zweitzauber, Tot/Geist/Offline- +-- Anzeige, Debuff-Icon mit Stackzahl -- -- Ehre wem Ehre gebuehrt: Aufbau, Namensplaketten und Grundidee stammen -- aus dem Original. @@ -18,19 +24,36 @@ FBHasSuperWoW = (SUPERWOW_VERSION ~= nil); FBClass = UnitClass("player"); -- [[ Globals ]] -- +-- Vorgabewerte. Die SavedVariables ersetzen diese Tabelle beim Laden +-- komplett, deshalb setzt FBHealBox_ApplyDefaults() fehlende Schluessel +-- nach dem Laden noch einmal nach. HealBox = { MaxButtons = 5, Scale = 1.0, AttachMode = 0, Active = 1, SpellChoice = {}, + ButtonSpacing = 2, -- px zwischen den Buttons (1..20) + RowSpacing = 4, -- px zwischen den Plaketten (1..20) + ManaBar = 1, -- Manabalken im Lebensbalken anzeigen + ShowPets = 1, -- Begleiter als eigene Plaketten anzeigen + ClassColors = 1, -- Namen in Klassenfarbe + RangeFade = 1, -- Plakette ausgrauen, wenn ausser Reichweite + WatchBuff = nil, -- Zaubername der Buff-Wache (nil = aus) + SpellChoiceR = {}, -- Rechtsklick-Belegung + RightClick = 0, -- Rechtsklick-Zweitzauber (explizit einschalten) + DebuffIcon = 1, -- Debuff-Icon neben dem Namen + LOSIcon = 1, -- Sichtlinien-Abzeichen + BuffWatchPets = 0, -- Buff-Wache auch fuer Begleiter + PlateLeft = "target", -- Klick auf die Plakette: target | menu | move | none + PlateRight = "target", }; -- Anzeigename des Addons. FBADDON_FOLDER muss dem Ordnernamen unter --- Interface\AddOns entsprechen (dort liegt auch die .toc) - nur dann +-- 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"; +HealBoxVersion = "|cFFFFFF00v1.4.1|r"; -- ========================================================================== -- [ Lokalisierung / Localization ] @@ -53,9 +76,9 @@ FBLocale["enUS"] = { LANG_NAME = "English", LANGUAGE = "Language", - LOADED = "loaded|r - the minimap button opens the options, /fbp reports the heal prediction.", + LOADED = "loaded|r. Minimap button or /fbp config 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", + 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", @@ -63,6 +86,27 @@ FBLocale["enUS"] = { SELECT_SPELL = "Select spell...", BUTTON = "Button", + TAB_BUTTONS = "Buttons", + TAB_GENERAL = "General", + COL_LEFT = "Left click", + COL_RIGHT = "Right click", + RIGHTCLICK = "Right-click spell", + RIGHTCLICK_TIP = "Gives every button a second spell on right click (e.g. Flash Heal left, Greater Heal right). Off by default. When on, a second column appears above and a small icon in the corner of each button shows the right-click spell.", + TT_RIGHT = "Right click", + STATE_DEAD = "Dead", + STATE_GHOST = "Ghost", + STATE_OFFLINE = "Offline", + PLATE_LEFT = "Left click on plate", + PLATE_RIGHT = "Right click on plate", + PLATE_TIP = "What a click on a name plate (name or health bar) does. Shift + left drag always moves the display, whatever is set here.", + ACT_TARGET = "Target", + ACT_MENU = "Unit menu", + ACT_MOVE = "Move display", + ACT_NONE = "Nothing", + LOSICON = "Line of sight", + LOSICON_TIP = "Shows an eye badge on the left edge of a plate while the unit is out of your line of sight. With the UnitXP client mod this is checked live; without it the addon remembers a 'not in line of sight' error for a few seconds after you tried to heal that unit and clears it as soon as a cast on the unit starts or a heal lands.", + DEBUFFICON = "Debuff icon", + DEBUFFICON_TIP = "Shows the icon of the first debuff your class can remove next to the name, with its stack count. The health bar keeps taking the debuff colour as well.", MENU_NO_SPELL = "|cFF999999No spell|r", RANK_DEFAULT = "Default", @@ -72,13 +116,35 @@ FBLocale["enUS"] = { SMALL = "Small", LARGE = "Large", + BTN_SPACING = "Button spacing: |cFFFFFFFF%s px", + ROW_SPACING = "Row spacing: |cFFFFFFFF%s px", + 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.", + MANABAR = "Mana bar", + MANABAR_TIP = "Shows a thin blue mana bar along the bottom edge of the health bar, only for units that actually use mana (no rage, focus or energy).", + SHOWPETS = "Show pets", + SHOWPETS_TIP = "Adds a plate for every pet in the group (hunter, warlock) directly below its owner, with the same heal buttons.", + TESTMODE = "Test mode", + TESTMODE_TIP = "Fills the display with ghost players and pets so you can arrange everything without being in a group. Ghosts show health, mana, a shield, incoming healing and a dispellable debuff. Not saved, always off after login.", + TEST_ON = "Test mode |cFF00FF00on|r, ghost players active. Buttons do not cast on ghosts.", + TEST_OFF = "Test mode |cFFFF0000off|r.", + TEST_CLICK = "Test mode: no cast on a ghost player.", + CLASSCOLORS = "Class colours", + CLASSCOLORS_TIP = "Colours the name on each plate in the class colour (Healium style). Pets keep their light-blue name.", + RANGEFADE = "Range fading", + RANGEFADE_TIP = "Fades a whole plate including its buttons to half transparency when the unit is out of range of your first assigned spell (or beyond 28 yards if no spell is assigned).", + BUFFWATCH = "Buff watch", + BUFFWATCH_TIP = "Pick one of your buffs. Every plate whose unit is missing that buff gets an orange border. The group version counts as well (Prayer of Fortitude for Power Word: Fortitude, Gift of the Wild for Mark of the Wild, Greater Blessings, ...).", + BUFFWATCH_NONE = "none", + BUFFWATCH_PETS = "Buff watch on pets", + BUFFWATCH_PETS_TIP = "Also marks pets that are missing the watched buff. Off by default, because pets rarely get Fortitude or Blessings, so their plates would stay orange most of the time.", + MENU_NO_BUFF = "|cFF999999No buff watch|r", 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", + 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:", @@ -98,6 +164,7 @@ FBLocale["enUS"] = { FBP_INCOMING = "Incoming", FBP_DEBUG = "Debug:", FBP_RESET = "Learned values discarded.", + FBP_COMMANDS = "Commands: /fbp config (options window), /fbp test (test mode), /fbp debug, /fbp reset", DBG_HOT = "HoT %s on %s: %d per tick, %ds", DBG_SHIELD = "Shield %s on %s: %d absorb, %ds", @@ -111,9 +178,9 @@ FBLocale["deDE"] = { LANG_NAME = "Deutsch", LANGUAGE = "Sprache", - LOADED = "geladen|r - der Minimap-Button oeffnet die Optionen, /fbp zeigt den Stand der Heilvorhersage.", + LOADED = "geladen|r. Minimap-Button oder /fbp config 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", + 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", @@ -121,6 +188,27 @@ FBLocale["deDE"] = { SELECT_SPELL = "Zauber waehlen...", BUTTON = "Button", + TAB_BUTTONS = "Buttons", + TAB_GENERAL = "Allgemein", + COL_LEFT = "Linksklick", + COL_RIGHT = "Rechtsklick", + RIGHTCLICK = "Rechtsklick-Zauber", + RIGHTCLICK_TIP = "Gibt jedem Button einen zweiten Zauber per Rechtsklick (z. B. Blitzheilung links, Grosse Heilung rechts). Standardmaessig aus. Eingeschaltet erscheint oben eine zweite Spalte, und ein kleines Icon in der Ecke jedes Buttons zeigt den Rechtsklick-Zauber.", + TT_RIGHT = "Rechtsklick", + STATE_DEAD = "Tot", + STATE_GHOST = "Geist", + STATE_OFFLINE = "Offline", + PLATE_LEFT = "Linksklick auf Plakette", + PLATE_RIGHT = "Rechtsklick auf Plakette", + PLATE_TIP = "Was ein Klick auf eine Namensplakette (Name oder Lebensbalken) tut. Shift + Linksklick ziehen verschiebt die Anzeige immer, egal was hier eingestellt ist.", + ACT_TARGET = "Anvisieren", + ACT_MENU = "Einheitenmenue", + ACT_MOVE = "Anzeige verschieben", + ACT_NONE = "Nichts", + LOSICON = "Sichtlinie", + LOSICON_TIP = "Zeigt ein Augen-Abzeichen am linken Rand einer Plakette, solange die Einheit ausserhalb deiner Sichtlinie ist. Mit dem Client-Mod UnitXP wird das live geprueft; ohne ihn merkt sich das Addon die Fehlermeldung 'nicht in Sichtlinie' nach einem Heilversuch fuer ein paar Sekunden und loescht sie, sobald ein Cast auf die Einheit startet oder eine Heilung ankommt.", + DEBUFFICON = "Debuff-Icon", + DEBUFFICON_TIP = "Zeigt das Icon des ersten von deiner Klasse entfernbaren Debuffs neben dem Namen, mit Stackzahl. Der Lebensbalken nimmt zusaetzlich weiterhin die Debuff-Farbe an.", MENU_NO_SPELL = "|cFF999999Kein Zauber|r", RANK_DEFAULT = "Standard", @@ -130,13 +218,35 @@ FBLocale["deDE"] = { SMALL = "Klein", LARGE = "Gross", + BTN_SPACING = "Button-Abstand: |cFFFFFFFF%s px", + ROW_SPACING = "Zeilen-Abstand: |cFFFFFFFF%s px", + 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.", + MANABAR = "Manabalken", + MANABAR_TIP = "Zeigt einen schmalen blauen Manabalken am unteren Rand des Lebensbalkens, nur bei Einheiten, die tatsaechlich Mana nutzen (kein Wut, Fokus oder Energie).", + SHOWPETS = "Begleiter anzeigen", + SHOWPETS_TIP = "Legt fuer jeden Begleiter in der Gruppe (Jaeger, Hexenmeister) eine eigene Plakette direkt unter seinem Besitzer an, mit denselben Heil-Buttons.", + TESTMODE = "Testmodus", + TESTMODE_TIP = "Fuellt die Anzeige mit Geisterspielern und -begleitern, damit du alles einrichten kannst, ohne in einer Gruppe zu sein. Die Geister zeigen Leben, Mana, einen Schild, eingehende Heilung und einen entfernbaren Debuff. Wird nicht gespeichert, nach dem Login immer aus.", + TEST_ON = "Testmodus |cFF00FF00an|r, Geisterspieler aktiv. Buttons casten nicht auf Geister.", + TEST_OFF = "Testmodus |cFFFF0000aus|r.", + TEST_CLICK = "Testmodus: kein Cast auf einen Geisterspieler.", + CLASSCOLORS = "Klassenfarben", + CLASSCOLORS_TIP = "Faerbt den Namen auf jeder Plakette in der Klassenfarbe (wie Healium). Begleiter behalten ihren hellblauen Namen.", + RANGEFADE = "Reichweiten-Fading", + RANGEFADE_TIP = "Blendet eine ganze Plakette samt Buttons auf halbe Deckkraft ab, wenn die Einheit ausserhalb der Reichweite deines ersten belegten Zaubers ist (ohne belegten Zauber: weiter als 28 Meter).", + BUFFWATCH = "Buff-Wache", + BUFFWATCH_TIP = "Waehle einen deiner Buffs. Jede Plakette, deren Einheit diesen Buff nicht hat, bekommt einen orangen Rahmen. Die Gruppenversion zaehlt mit (Gebet der Seelenstaerke fuer Machtwort: Seelenstaerke, Gabe der Wildnis fuer Mal der Wildnis, Grosse Segen, ...).", + BUFFWATCH_NONE = "keiner", + BUFFWATCH_PETS = "Buff-Wache auch fuer Begleiter", + BUFFWATCH_PETS_TIP = "Markiert auch Begleiter, denen der ueberwachte Buff fehlt. Standardmaessig aus, denn Pets bekommen selten Seelenstaerke oder Segen, ihre Plaketten waeren sonst meist orange.", + MENU_NO_BUFF = "|cFF999999Keine Buff-Wache|r", 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", + 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:", @@ -156,6 +266,7 @@ FBLocale["deDE"] = { FBP_INCOMING = "Fremdheilung", FBP_DEBUG = "Debug:", FBP_RESET = "Gelernte Werte verworfen.", + FBP_COMMANDS = "Befehle: /fbp config (Optionsfenster), /fbp test (Testmodus), /fbp debug, /fbp reset", DBG_HOT = "HoT %s auf %s: %d pro Tick, %ds", DBG_SHIELD = "Schild %s auf %s: %d Absorb, %ds", @@ -190,15 +301,249 @@ function FBSetLocale(code, apply) end FBL = FBLocale[FBDetectLocale()]; -xSpacing = 2; LowHP = 0.6; VeryLowHP = 0.3; NamePlateWidth = 120; NamePlateHeight = 28; + +-- Manabalken: "Balken im Balken" am unteren Rand des Lebensbalkens +FBMANA_BAR_HEIGHT = 5; -- px +FBMANA_BAR_COLOR = { 0.15, 0.40, 1.00, 1 }; -- blau +FBMANA_BG_ALPHA = 0.35; -- dunkler Streifen hinter dem Mana (0 = aus) + +-- Begleiter-Plaketten: Namensfarbe, Einrueckung unter dem Besitzer (die +-- Plakette wird um denselben Betrag schmaler, damit die Buttons buendig +-- bleiben) und ein kleines Pfoten-Icon vor dem Namen. +FBPET_NAME_COLOR = { 0.75, 0.85, 1.00, 1 }; +FBPET_INDENT = 12; +FBPET_ICON = "Interface\\Icons\\INV_Misc_Foot_Kodo"; +FBPET_ICON_SIZE = 12; + +-- Debuff-Icon neben dem Namen +FBDEBUFF_ICON_SIZE = 14; + +-- Breite der Namensbox: normal, und wenn rechts daneben das Debuff-Icon steht +-- (rechts davon braucht der Prozenttext etwa 34 px) +FBNAME_WIDTH_FULL = NamePlateWidth - 42; -- 78 +FBNAME_WIDTH_ICON = NamePlateWidth - 42 - FBDEBUFF_ICON_SIZE - 4; -- 60 + +-- Sichtlinien-Abzeichen: Icon, Groesse, Position (Anker RIGHT an LEFT der Plakette) +FBLOS_ICON = "Interface\\Icons\\Spell_Shadow_MindSteal"; -- das "blinde Auge" (Blenden) +FBLOS_ICON_SIZE = 16; +FBLOS_ICON_X = 6; -- wie weit das Abzeichen in die Plakette hineinragt +FBLOS_ICON_Y = 0; +FBLOS_TIMEOUT = 8; -- Sek., wie lange eine LoS-Fehlermeldung ohne UnitXP gilt + +-- Reichweiten-Fading: Deckkraft einer Plakette ausser Reichweite, Pruefintervall +FBRANGE_ALPHA = 0.5; +FBRANGE_INTERVAL = 0.5; -- Sekunden + +-- Rahmenfarbe, wenn der ueberwachte Buff fehlt (Buff-Wache) +FBBUFF_MISSING_COLOR = { 1.0, 0.5, 0.0, 1 }; +FBBUFF_NORMAL_COLOR = { 1.0, 1.0, 1.0, 1 }; + +-- Klassenfarben: RAID_CLASS_COLORS des Clients, sonst diese Vorgaben +FBClassColors = { + WARRIOR = { r = 0.78, g = 0.61, b = 0.43 }, + PALADIN = { r = 0.96, g = 0.55, b = 0.73 }, + HUNTER = { r = 0.67, g = 0.83, b = 0.45 }, + ROGUE = { r = 1.00, g = 0.96, b = 0.41 }, + PRIEST = { r = 1.00, g = 1.00, b = 1.00 }, + SHAMAN = { r = 0.00, g = 0.44, b = 0.87 }, + MAGE = { r = 0.41, g = 0.80, b = 0.94 }, + WARLOCK = { r = 0.58, g = 0.51, b = 0.79 }, + DRUID = { r = 1.00, g = 0.49, b = 0.04 }, +}; + +function FBClassColor(token) + if (not token) then return nil; end + if (RAID_CLASS_COLORS and RAID_CLASS_COLORS[token]) then return RAID_CLASS_COLORS[token]; end + return FBClassColors[token]; +end + +-- Buff-Wache: welche Buffs der eigenen Klasse zur Auswahl stehen (nur die +-- gelernten erscheinen im Menue) und welche Gruppenversion dasselbe zaehlt. +FBBuffWatchSpells = { + Priest = { "Power Word: Fortitude", "Prayer of Fortitude", "Divine Spirit", "Prayer of Spirit", + "Shadow Protection", "Prayer of Shadow Protection", "Renew", "Power Word: Shield" }, + Druid = { "Mark of the Wild", "Gift of the Wild", "Thorns", "Rejuvenation", "Regrowth" }, + Paladin = { "Blessing of Wisdom", "Greater Blessing of Wisdom", "Blessing of Might", "Greater Blessing of Might", + "Blessing of Kings", "Greater Blessing of Kings", "Blessing of Salvation", "Greater Blessing of Salvation", + "Blessing of Light", "Greater Blessing of Light", "Blessing of Sanctuary", "Greater Blessing of Sanctuary" }, + Shaman = { "Earth Shield", "Water Shield" }, +}; +FBBuffAlternates = { + ["Power Word: Fortitude"] = "Prayer of Fortitude", ["Prayer of Fortitude"] = "Power Word: Fortitude", + ["Divine Spirit"] = "Prayer of Spirit", ["Prayer of Spirit"] = "Divine Spirit", + ["Shadow Protection"] = "Prayer of Shadow Protection", ["Prayer of Shadow Protection"] = "Shadow Protection", + ["Mark of the Wild"] = "Gift of the Wild", ["Gift of the Wild"] = "Mark of the Wild", + ["Blessing of Wisdom"] = "Greater Blessing of Wisdom", ["Greater Blessing of Wisdom"] = "Blessing of Wisdom", + ["Blessing of Might"] = "Greater Blessing of Might", ["Greater Blessing of Might"] = "Blessing of Might", + ["Blessing of Kings"] = "Greater Blessing of Kings", ["Greater Blessing of Kings"] = "Blessing of Kings", + ["Blessing of Salvation"] = "Greater Blessing of Salvation", ["Greater Blessing of Salvation"] = "Blessing of Salvation", + ["Blessing of Light"] = "Greater Blessing of Light", ["Greater Blessing of Light"] = "Blessing of Light", + ["Blessing of Sanctuary"] = "Greater Blessing of Sanctuary", ["Greater Blessing of Sanctuary"] = "Blessing of Sanctuary", +}; +FBBuffSpells = {}; -- [Name] = { icon = Textur } (aus dem Zauberbuch) + +-- ========================================================================== +-- [ Slots ] +-- +-- Zehn Plaketten: 1-5 Spieler (player, party1-4), 6-10 deren Begleiter +-- (pet, partypet1-4). FBPartyFrame[p] ist die Plakette, FBPartyTable[p] +-- ihre Buttons, FBPartyUnit[p] die Unit-ID. Angezeigt wird in der +-- Reihenfolge FBLayoutOrder: jeder Begleiter direkt unter seinem Besitzer. +-- ========================================================================== + +FBSlotCount = 10; +FBPartyUnit = { "player", "party1", "party2", "party3", "party4", + "pet", "partypet1", "partypet2", "partypet3", "partypet4" }; +FBSlotIsPet = { false, false, false, false, false, true, true, true, true, true }; +FBLayoutOrder = { 1, 6, 2, 7, 3, 8, 4, 9, 5, 10 }; + +-- Unit-ID -> Slot (fuer die UNIT_*-Events) +FBUnitSlot = {}; +for p = 1, FBSlotCount do FBUnitSlot[FBPartyUnit[p]] = p; end + +FBPartyFrame = {}; +FBPartyTable = {}; +for p = 1, FBSlotCount do FBPartyTable[p] = {}; end + +-- ========================================================================== +-- [ Testmodus: Geisterspieler ] +-- +-- Im Testmodus bleibt Slot 1 (der Spieler selbst) echt, alle anderen Slots +-- werden mit Geistern gefuellt, auch wenn gerade eine echte Gruppe da ist. +-- Die Geister atmen: ihre HP schwanken langsam, damit die Farbschwellen, +-- Schild- und Vorhersage-Schichten sichtbar werden. Die Anzeige liest +-- Einheiten ausschliesslich ueber die FBUnit*-Wrapper unten, deshalb muss +-- der restliche Code den Testmodus nicht kennen. +-- Der Testmodus wird bewusst nicht gespeichert. +-- ========================================================================== + +FBTestMode = false; + +-- hp/mp sind Anteile (0..1), swing ist die Amplitude der HP-Schwankung, +-- debuff = true gibt dem Geist einen von der eigenen Klasse entfernbaren Debuff +-- (Icon debuffTex, debuffCount Stacks), +-- buffMissing = true laesst die Buff-Wache anschlagen, outOfRange = true faded, +-- los = true zeigt das Sichtlinien-Abzeichen. +FBTestGhosts = { + ["party1"] = { name = "Brynn", class = "WARRIOR", hpMax = 3400, hp = 0.90, swing = 0.08, hasMana = false, buffMissing = true }, + ["party2"] = { name = "Cerys", class = "WARLOCK", hpMax = 2300, hp = 0.55, swing = 0.10, hasMana = true, mpMax = 3100, mp = 0.65, shield = 450, los = true }, + ["party3"] = { name = "Dorn", class = "HUNTER", hpMax = 2900, hp = 0.30, swing = 0.15, hasMana = true, mpMax = 2400, mp = 0.35, inc = 700 }, + ["party4"] = { name = "Elowen", class = "DRUID", hpMax = 2700, hp = 0.95, swing = 0.04, hasMana = true, mpMax = 3600, mp = 0.90, debuff = true, debuffTex = "Interface\\Icons\\Spell_Shadow_ShadowWordPain", debuffCount = 3, outOfRange = true }, + ["pet"] = { name = "Fang", hpMax = 1900, hp = 0.75, swing = 0.12, hasMana = false }, + ["partypet2"] = { name = "Zorbek", hpMax = 1200, hp = 0.60, swing = 0.10, hasMana = true, mpMax = 900, mp = 0.50 }, + ["partypet3"] = { name = "Bramble", hpMax = 2100, hp = 0.40, swing = 0.14, hasMana = false, inc = 300 }, +}; + +function FBTest_Ghost(unit) + if (not FBTestMode) or (unit == "player") then return nil; end + return FBTestGhosts[unit]; +end + +-- Aktueller HP-Anteil eines Geistes (langsame Sinus-Schwankung) +function FBTest_HPFraction(g, unit) + local phase = string.len(unit) * 1.7; + local frac = g.hp + (g.swing or 0) * math.sin((GetTime() * 0.6) + phase); + if (frac < 0.03) then frac = 0.03; end + if (frac > 1) then frac = 1; end + return frac; +end + +function FBTest_Set(on) + FBTestMode = on and true or false; + if (TestModeCheck) then TestModeCheck:SetChecked(FBTestMode); end + if (FBTestMode) then + -- ausgeblendete Anzeige einblenden, sonst sieht man nichts + if (HealBox.Active ~= 1) then HealBox.Active = 1; end + DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..FBT("TEST_ON")); + else + DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..FBT("TEST_OFF")); + end + FBUpdateNames(); +end + +-- [ Unit-Wrapper: echte Einheit oder Geist ] -------------------------------- + +function FBUnitExists(unit) + if (FBTestMode and unit ~= "player") then + return (FBTestGhosts[unit] ~= nil); + end + return UnitExists(unit); +end + +function FBUnitName(unit) + local g = FBTest_Ghost(unit); + if (g) then return g.name; end + return UnitName(unit); +end + +-- liefert hp, hpMax (hpMax nie 0, damit keine Division durch Null entsteht) +function FBUnitHealth(unit) + local g = FBTest_Ghost(unit); + if (g) then + return math.floor(g.hpMax * FBTest_HPFraction(g, unit)), g.hpMax; + end + local hp, hpMax = UnitHealth(unit), UnitHealthMax(unit); + if (not hpMax) or (hpMax <= 0) then hpMax = 1; end + return (hp or 0), hpMax; +end + +-- liefert mp, mpMax, hasMana. hasMana ist nur wahr, wenn die Einheit +-- tatsaechlich Mana nutzt (Powertyp 0). Krieger, Schurken, Druiden in +-- Gestalt und Jaegerbegleiter bekommen keinen Manabalken. +function FBUnitMana(unit) + local g = FBTest_Ghost(unit); + if (g) then + if (not g.hasMana) then return 0, 0, false; end + return math.floor(g.mpMax * g.mp), g.mpMax, true; + end + local ptype = 0; + if (UnitPowerType) then ptype = UnitPowerType(unit); end + if (ptype ~= 0) then return 0, 0, false; end + local mp, mpMax = UnitMana(unit), UnitManaMax(unit); + if (not mpMax) or (mpMax <= 0) then return 0, 0, false; end + return (mp or 0), mpMax, true; +end + +-- nil (lebt), "dead", "ghost" oder "offline" +function FBUnitState(unit) + local g = FBTest_Ghost(unit); + if (g) then return g.state; end + if (UnitIsConnected and not UnitIsConnected(unit)) then return "offline"; end + if (UnitIsGhost and UnitIsGhost(unit)) then return "ghost"; end + if (UnitIsDead and UnitIsDead(unit)) then return "dead"; end + return nil; +end + +-- Klassen-Token in Grossbuchstaben ("PRIEST"), nil bei Begleitern/unbekannt +function FBUnitClassToken(unit) + local g = FBTest_Ghost(unit); + if (g) then return g.class; end + local loc, eng = UnitClass(unit); + if (eng) then return strupper(eng); end + if (loc) then return strupper(loc); end + return nil; +end FBDropDown = {}; FBDropDownButtonValue = {}; FBDropDownButton = {}; FBDropDownButtonIcon = {}; +-- Rechtsklick-Belegung (zweiter Zauber je Button) +FBDropDownButtonR = {}; +FBDropDownButtonIconR = {}; +FBActiveSpellIDsR = {}; +FBSpellBtnsR = {}; + +-- Die drei Laufzeit-Tabellen und die gespeicherte Tabelle einer Seite +function FBChoiceTables(side) + if (side == "R") then + return FBDropDownButtonR, FBDropDownButtonIconR, FBActiveSpellIDsR, FBSpellBtnsR, HealBox.SpellChoiceR; + end + return FBDropDownButton, FBDropDownButtonIcon, FBActiveSpellIDs, FBSpellBtns, HealBox.SpellChoice; +end ClassIcon = { Druid = "Interface/Icons/INV_Misc_MonsterClaw_04", @@ -263,11 +608,6 @@ if (FBClass == "Paladin") then Spell.Name[7] = "Blessing of Protection"; end -FBParty1 = {}; -FBParty2 = {}; -FBParty3 = {}; -FBParty4 = {}; -FBParty5 = {}; MaxButtonCount = 10; -- Speichert alle verfügbaren Zauber und Ränge @@ -287,11 +627,66 @@ function FBHealBox_OnLoad() this:RegisterEvent("PLAYER_ENTERING_WORLD"); this:RegisterEvent("SPELLS_CHANGED"); this:RegisterEvent("UNIT_HEALTH"); + this:RegisterEvent("UNIT_MAXHEALTH"); this:RegisterEvent("SPELL_UPDATE_COOLDOWN"); this:RegisterEvent("VARIABLES_LOADED"); this:RegisterEvent("UNIT_AURA"); + -- Begleiter kommen und gehen (Beschwoerung, Wegschicken, Tod) + this:RegisterEvent("UNIT_PET"); + this:RegisterEvent("UNIT_NAME_UPDATE"); + -- Manabalken + this:RegisterEvent("UNIT_MANA"); + this:RegisterEvent("UNIT_MAXMANA"); + this:RegisterEvent("UNIT_DISPLAYPOWER"); end +-- Fehlende Schluessel in den geladenen SavedVariables nachziehen +function FBHealBox_ApplyDefaults() + if (not HealBox) then HealBox = {}; end + if (not HealBox.SpellChoice) then HealBox.SpellChoice = {}; end + if (HealBox.MaxButtons == nil) then HealBox.MaxButtons = 5; end + if (HealBox.Scale == nil) then HealBox.Scale = 1.0; end + if (HealBox.AttachMode == nil) then HealBox.AttachMode = 0; 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 + if (HealBox.ButtonSpacing == nil) then HealBox.ButtonSpacing = 2; end + if (HealBox.RowSpacing == nil) then HealBox.RowSpacing = 4; end + if (HealBox.ManaBar == nil) then HealBox.ManaBar = 1; end + if (HealBox.ShowPets == nil) then HealBox.ShowPets = 1; end + if (HealBox.ClassColors == nil) then HealBox.ClassColors = 1; end + if (HealBox.RangeFade == nil) then HealBox.RangeFade = 1; end + if (not HealBox.SpellChoiceR) then HealBox.SpellChoiceR = {}; end + if (HealBox.RightClick == nil) then HealBox.RightClick = 0; end + if (HealBox.DebuffIcon == nil) then HealBox.DebuffIcon = 1; end + if (HealBox.LOSIcon == nil) then HealBox.LOSIcon = 1; end + if (HealBox.BuffWatchPets == nil) then HealBox.BuffWatchPets = 0; end + if (not FBPlateActionName[HealBox.PlateLeft or ""]) then HealBox.PlateLeft = "target"; end + if (not FBPlateActionName[HealBox.PlateRight or ""]) then HealBox.PlateRight = "target"; end +end + +-- Optionsfenster an die gespeicherten Werte angleichen +function FBHealBox_SyncOptions() + if (MaxButtonSlider) then MaxButtonSlider:SetValue(HealBox.MaxButtons); end + if (ScaleSlider) then ScaleSlider:SetValue(HealBox.Scale); end + if (ButtonSpacingSlider) then ButtonSpacingSlider:SetValue(HealBox.ButtonSpacing); end + if (RowSpacingSlider) then RowSpacingSlider:SetValue(HealBox.RowSpacing); end + if (AttachModeCheck) then AttachModeCheck:SetChecked(HealBox.AttachMode == 1); end + if (HealCommCheck) then HealCommCheck:SetChecked(HealBox.HealComm == 1); end + if (ManaBarCheck) then ManaBarCheck:SetChecked(HealBox.ManaBar == 1); end + if (ShowPetsCheck) then ShowPetsCheck:SetChecked(HealBox.ShowPets == 1); end + if (TestModeCheck) then TestModeCheck:SetChecked(FBTestMode); end + if (ClassColorsCheck) then ClassColorsCheck:SetChecked(HealBox.ClassColors == 1); end + if (RangeFadeCheck) then RangeFadeCheck:SetChecked(HealBox.RangeFade == 1); end + if (DebuffIconCheck) then DebuffIconCheck:SetChecked(HealBox.DebuffIcon == 1); end + if (LOSIconCheck) then LOSIconCheck:SetChecked(HealBox.LOSIcon == 1); end + if (BuffWatchPetsCheck) then BuffWatchPetsCheck:SetChecked(HealBox.BuffWatchPets == 1); end + if (RightClickCheck) then RightClickCheck:SetChecked(HealBox.RightClick == 1); end + FBHealBox_UpdateBuffWatchLabel(); + FBHealBox_UpdatePlateActionLabels(); + FBHealBox_ApplyRightClickLayout(); +end + function FBUnitGUID(unit) if (FBHasSuperWoW and UnitExists(unit)) then local exists, guid = UnitExists(unit); @@ -303,6 +698,7 @@ end -- [ Zauberbuch scannen und Ränge sammeln ] -- function FBLoadSpellData() FBPlayerSpells = {}; + FBBuffSpells = {}; local i = 1; while true do local spellName, spellRank = GetSpellName(i, BOOKTYPE_SPELL); @@ -314,6 +710,13 @@ function FBLoadSpellData() break; end end + -- Buff-Wache: Textur des Zaubers merken (rangunabhaengig) + for _, v in ipairs(FBBuffWatchSpells[FBClass] or {}) do + if (v == spellName) then + FBBuffSpells[spellName] = { icon = GetSpellTexture(i, BOOKTYPE_SPELL) }; + break; + end + end if isHealBoxSpell then if not FBPlayerSpells[spellName] then @@ -328,22 +731,28 @@ function FBLoadSpellData() -- 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; + if (not HealBox.SpellChoiceR) then HealBox.SpellChoiceR = {}; end + for _, side in ipairs({ "L", "R" }) do + local _, _, _, _, savedTable = FBChoiceTables(side); + for btnIndex = 1, MaxButtonCount, 1 do + local saved = savedTable[btnIndex]; + if type(saved) == "number" then + savedTable[btnIndex] = nil; + saved = nil; + end + FBApplySpellChoice(btnIndex, saved, side); end - FBApplySpellChoice(btnIndex, saved); end end -function FBApplySpellChoice(i, castString) +-- side = "L" (Standard) oder "R" (Rechtsklick) +function FBApplySpellChoice(i, castString, side) if type(castString) == "number" then return; end + local names, icons, ids, fields = FBChoiceTables(side); - FBDropDownButton[i] = castString; - FBDropDownButtonIcon[i] = "Interface\\Icons\\INV_Misc_QuestionMark"; - FBActiveSpellIDs[i] = nil; + names[i] = castString; + icons[i] = "Interface\\Icons\\INV_Misc_QuestionMark"; + ids[i] = nil; if castString then for baseName, ranks in pairs(FBPlayerSpells) do @@ -353,29 +762,28 @@ function FBApplySpellChoice(i, castString) cmpString = baseName .. "(" .. spellData.rank .. ")"; end if cmpString == castString then - FBDropDownButtonIcon[i] = spellData.icon; - FBActiveSpellIDs[i] = spellData.id; + icons[i] = spellData.icon; + ids[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(); + if fields[i] then + if castString and icons[i] then + fields[i].text:SetText(castString); + fields[i].icon:SetTexture(icons[i]); else - FBSpellBtns[i].text:SetText(FBT("SELECT_SPELL")); - FBSpellBtns[i].icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark"); - FBSpellBtns[i].icon:Show(); + fields[i].text:SetText(FBT("SELECT_SPELL")); + fields[i].icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark"); end + fields[i].icon:Show(); end end -- ========================================================================== --- [ FB Cascading Menu - eigenes Menuesystem fuer Vanilla 1.12 / Lua 5.0 ] +-- [ 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 @@ -401,6 +809,7 @@ FBMENU_GRACE_TIME = 3.0; -- Sek. ausserhalb -> Auto-Close (999 = aus) FBMenuList = {}; -- [level] = Frame FBMenuActiveButtonID = 1; -- welcher Options-Slot wird gerade belegt +FBMenuActiveSide = "L"; -- "L" = Linksklick-Feld, "R" = Rechtsklick-Feld FBMenuOpenSubValue = nil; -- welcher Zauber haengt gerade im Untermenue FBMenuOutTimer = 0; @@ -648,14 +1057,16 @@ function FBMenu_OpenMenu(entries, anchorFrame) FBMenuDriver:Show(); end -function FBMenu_OpenSpellMenu(btnIndex, anchorFrame) - -- gleicher Slot nochmal geklickt -> zu - if (FBMenu_IsOpen() and FBMenuActiveButtonID == btnIndex) then +function FBMenu_OpenSpellMenu(btnIndex, anchorFrame, side) + side = side or "L"; + -- gleiches Feld nochmal geklickt -> zu + if (FBMenu_IsOpen() and FBMenuActiveButtonID == btnIndex and FBMenuActiveSide == side) then FBMenu_CloseAll(); return; end FBMenuActiveButtonID = btnIndex; + FBMenuActiveSide = side; FBMenu_OpenMenu(FBMenu_BuildSpellEntries(), anchorFrame); end @@ -681,6 +1092,61 @@ function FBMenu_OpenLanguageMenu(anchorFrame) FBMenu_OpenMenu(entries, anchorFrame); end +-- Buff-Wache: Auswahl eines gelernten Buffs (oder keiner) +function FBMenu_OpenBuffMenu(anchorFrame) + if (FBMenu_IsOpen()) then + FBMenu_CloseAll(); + return; + end + + local entries = {}; + local none = {}; + none.text = FBT("MENU_NO_BUFF"); + none.icon = "Interface\\Icons\\INV_Misc_QuestionMark"; + none.func = function() + FBHealBox_SetWatchBuff(nil); + FBMenu_CloseAll(); + end + table.insert(entries, none); + + for _, spellName in ipairs(FBBuffWatchSpells[FBClass] or {}) do + local sd = FBBuffSpells[spellName]; + if (sd) then + local e = {}; + e.text = spellName; + e.icon = sd.icon; + e.value = spellName; + e.func = function(entry) + FBHealBox_SetWatchBuff(entry.value); + FBMenu_CloseAll(); + end + table.insert(entries, e); + end + end + FBMenu_OpenMenu(entries, anchorFrame); +end + +-- Klickaktion der Plakette waehlen (side = "L" / "R") +function FBMenu_OpenPlateActionMenu(anchorFrame, side) + if (FBMenu_IsOpen()) then + FBMenu_CloseAll(); + return; + end + local entries = {}; + for _, action in ipairs(FBPlateActionOrder) do + local e = {}; + e.text = FBT(FBPlateActionName[action]); + e.action = action; + e.func = function(entry) + if (side == "R") then HealBox.PlateRight = entry.action; else HealBox.PlateLeft = entry.action; end + FBHealBox_UpdatePlateActionLabels(); + FBMenu_CloseAll(); + end + table.insert(entries, e); + end + FBMenu_OpenMenu(entries, anchorFrame); +end + -- [ Inhalte ] -------------------------------------------------------------- function FBMenu_MakeCastString(spellName, rank) @@ -750,38 +1216,30 @@ end function FBMenu_SelectSpell(entry) local btnID = FBMenuActiveButtonID; - HealBox.SpellChoice[btnID] = entry.value; - FBApplySpellChoice(btnID, entry.value); + local _, _, _, _, savedTable = FBChoiceTables(FBMenuActiveSide); + savedTable[btnID] = entry.value; + FBApplySpellChoice(btnID, entry.value, FBMenuActiveSide); FBHealBoxButtonsChanged(); FBMenu_CloseAll(); end function FBMenu_ClearSpell() local btnID = FBMenuActiveButtonID; - HealBox.SpellChoice[btnID] = nil; - FBApplySpellChoice(btnID, nil); + local _, _, _, _, savedTable = FBChoiceTables(FBMenuActiveSide); + savedTable[btnID] = nil; + FBApplySpellChoice(btnID, nil, FBMenuActiveSide); 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 + FBHealBox_ApplyDefaults(); FBSetLocale(HealBox.Locale, 1); - FBUpdateNames(); - MaxButtonSlider:SetValue(HealBox.MaxButtons); + FBHealBox_SyncOptions(); HealBoxAttachMode(HealBox.AttachMode); + FBHealBox_ApplyButtonSpacing(); + FBUpdateNames(); end if (event == "PLAYER_ENTERING_WORLD" or event == "SPELLS_CHANGED") then @@ -790,124 +1248,233 @@ function FBHealBox_OnEvent(event, arg1) if (FBRegisterMinimapButtonWithMBB) then FBRegisterMinimapButtonWithMBB(); end + FBHealBox_UpdateBuffWatchLabel(); + if (event == "PLAYER_ENTERING_WORLD") then FBUpdateNames(); 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 + FBHealBox_ApplyDefaults(); FBSetLocale(HealBox.Locale, 1); FBLoadSpellData(); FBHealBoxButtons(); - HealBoxScale(FBHealBox1, HealBox.Scale); - ScaleSlider:SetValue(HealBox.Scale); - AttachModeCheck:SetChecked(HealBox.AttachMode); - HealCommCheck:SetChecked(HealBox.HealComm == 1); + FBHealBox_SyncOptions(); + HealBoxAttachMode(HealBox.AttachMode); + FBHealBox_ApplyButtonSpacing(); FBUpdateNames(); - if (HealBox.AttachMode == 0) then - FBHealBox1:SetScale(HealBox.Scale); - else - FBHealBox1:SetScale(1); + end + + -- Gruppe oder Begleiter geaendert -> Plaketten neu belegen und anordnen + if (event == "PARTY_MEMBERS_CHANGED" or event == "UNIT_PET") then + FBUpdateNames(); + end + + if (event == "UNIT_NAME_UPDATE") then + local p = FBUnitSlot[arg1]; + if (p and FBPartyFrame[p] and not FBTestMode) then + FBPartyFrame[p].NameText:SetText(strupper(UnitName(arg1) or "")); 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 + -- Alle UNIT_*-Events laufen ueber die Slot-Tabelle: ein Handler fuer + -- Spieler und Begleiter, Leben und Mana. Im Testmodus sind die Geister + -- vom Echtzeit-Update abgekoppelt (das erledigt FBPredict_OnUpdate). + if (event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" or event == "UNIT_AURA" + or event == "UNIT_MANA" or event == "UNIT_MAXMANA" or event == "UNIT_DISPLAYPOWER") then + local p = FBUnitSlot[arg1]; + if (p and FBPartyFrame[p]) then + FBHealBox_UpdateUnit(arg1, FBPartyFrame[p]); + if (event == "UNIT_AURA") then FBHealBox_CheckWatchBuff(arg1, FBPartyFrame[p]); end + end end end -- Alle drei Balken in dieselbe Ebene legen und stabil stapeln: -- HP deckend oben, darunter der Schild-Anteil, ganz unten die Heilvorhersage. +-- Der Manabalken liegt als oberste Schicht ueber dem unteren Rand des HP-Balkens. 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.ManaBar:SetFrameStrata(strata); f.IncHealBar:SetFrameLevel(1); f.ShieldBar:SetFrameLevel(2); f.HealthBar:SetFrameLevel(3); + f.ManaBar:SetFrameLevel(4); end -function FBHealBoxCreateFrame(FrameName,ParentFrame,FrameTexture,FrameWidth,FrameHeight,FrameAlpha,Unit) +-- Alle Balken einer Plakette ein-/ausblenden (Party-Frame-Modus blendet aus) +function FBHealBox_SetPlateVisible(f, visible) + if (not f) or (not f.HealthBar) then return; end + f.plateHidden = (not visible); + if (visible) then + f.NameText:Show(); f.HPText:Show(); + if (f.isPet and f.PetIcon) then f.PetIcon:Show(); end + f.HealthBar:Show(); f.ShieldBar:Show(); f.IncHealBar:Show(); + -- ManaBar entscheidet FBHealBox_UpdateMana je nach Powertyp + else + f.NameText:Hide(); f.HPText:Hide(); + if (f.isPet and f.PetIcon) then f.PetIcon:Hide(); end + f.HealthBar:Hide(); f.ShieldBar:Hide(); f.IncHealBar:Hide(); f.ManaBar:Hide(); + f.DebuffIcon:Hide(); f.DebuffCount:Hide(); f.LOSIcon:Hide(); + end +end + +-- ========================================================================== +-- [ Klick auf die Plakette ] +-- +-- Links- und Rechtsklick auf Name/Lebensbalken sind belegbar (Optionen, +-- Reiter Allgemein): target = anvisieren, menu = Blizzards Einheitenmenue, +-- move = Anzeige ziehen, none = nichts. Shift + Linksklick ziehen verschiebt +-- die Anzeige immer. +-- ========================================================================== + +FBPlateActionOrder = { "target", "menu", "move", "none" }; +FBPlateActionName = { target = "ACT_TARGET", menu = "ACT_MENU", move = "ACT_MOVE", none = "ACT_NONE" }; +FBHealBoxDragging = false; + +function FBHealBox_PlateAction(mouseButton) + if (mouseButton == "RightButton") then return HealBox.PlateRight or "target"; end + return HealBox.PlateLeft or "target"; +end + +-- Blizzards Dropdown zur Einheit (fuer das Einheitenmenue), sonst nil +function FBHealBox_UnitDropDown(unit) + if (unit == "player") then return PlayerFrameDropDown; end + local _, _, n = string.find(unit, "^party(%d)$"); + if (n) then return getglobal("PartyMemberFrame"..n.."DropDown"); end + if (unit == "pet") then return PetFrameDropDown; end + return nil; +end + +function FBHealBox_RunPlateAction(f, action) + local unit = f.unit; + if (not unit) or (action == "none") or (action == "move") then return; end + if (FBTest_Ghost(unit)) then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..FBT("TEST_CLICK")); + return; + end + if (not UnitExists(unit)) then return; end + + if (action == "menu") then + local dd = FBHealBox_UnitDropDown(unit); + if (dd and ToggleDropDownMenu) then + ToggleDropDownMenu(1, nil, dd, "cursor"); + return; + end + -- kein Menue fuer diese Einheit (Gruppen-Begleiter): anvisieren + end + TargetUnit(unit); +end + +function FBHealBox_PlateMouseDown(f) + local action = FBHealBox_PlateAction(arg1); + if (HealBox.AttachMode ~= 1) and (action == "move" or (arg1 == "LeftButton" and IsShiftKeyDown())) then + FBHealBoxDragging = true; + FBHealBox1:StartMoving(); + end +end + +function FBHealBox_PlateMouseUp(f) + if (FBHealBoxDragging) then + FBHealBoxDragging = false; + FBHealBox1:StopMovingOrSizing(); + FBHealBox_SavePosition(); + return; + end + FBHealBox_RunPlateAction(f, FBHealBox_PlateAction(arg1)); +end + +-- [ Plattenposition merken ] ------------------------------------------------ +-- Gespeichert wird die linke obere Ecke in Bildschirmpixeln (skalierungs- +-- unabhaengig), damit ein Wechsel der Skalierung die Platte nicht verschiebt. + +function FBHealBox_SavePosition() + if (not FBHealBox1) or (HealBox.AttachMode == 1) then return; end + local left, top = FBHealBox1:GetLeft(), FBHealBox1:GetTop(); + if (not left) or (not top) then return; end + local s = FBHealBox1:GetEffectiveScale(); + HealBox.PosX = left * s; + HealBox.PosY = top * s; +end + +function FBHealBox_RestorePosition() + if (not FBHealBox1) then return; end + FBHealBox1:ClearAllPoints(); + if (HealBox.PosX and HealBox.PosY) then + local s = FBHealBox1:GetEffectiveScale(); + FBHealBox1:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", HealBox.PosX / s, HealBox.PosY / s); + else + FBHealBox1:SetPoint("LEFT", WorldFrame, "LEFT", 100, 22); + end +end + +function FBHealBoxCreateFrame(FrameName,ParentFrame,FrameTexture,FrameWidth,FrameHeight,FrameAlpha,Unit,isPet) 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.unit = Unit; + f.isPet = isPet; + -- Begleiter: eingerueckt und um die Einrueckung schmaler + f.indent = 0; + if (isPet) then f.indent = FBPET_INDENT; end + f.plateW = FrameWidth - f.indent; + local barW = f.plateW - 5; + -- Breite der Namensbox ohne / mit Debuff-Icon (Pets: minus Pfoten-Icon) + local nameX = 4; + if (isPet) then nameX = 4 + FBPET_ICON_SIZE + 3; end + f.nameWidthFull = FBNAME_WIDTH_FULL - f.indent - (nameX - 4); + f.nameWidthIcon = FBNAME_WIDTH_ICON - f.indent - (nameX - 4); + f:SetAlpha(FrameAlpha); f:SetHeight(FrameHeight); - f:SetWidth(FrameWidth); + f:SetWidth(f.plateW); + if (isPet) then + f.PetIcon = f:CreateTexture(nil, "OVERLAY"); + f.PetIcon:SetWidth(FBPET_ICON_SIZE); + f.PetIcon:SetHeight(FBPET_ICON_SIZE); + f.PetIcon:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -7); + f.PetIcon:SetTexture(FBPET_ICON); + f.PetIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93); + end + + -- Name: linksbuendig, volle Breite. Nur solange ein Debuff-Icon zu sehen + -- ist, wird die Box schmaler (FBHealBox_UpdateDebuffIcon). f.NameText = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); - f.NameText:SetPoint("TOPLEFT", f, "TOPRIGHT", -120, -8); + f.NameText:SetPoint("TOPLEFT", f, "TOPLEFT", nameX, -8); + f.NameText:SetJustifyH("LEFT"); f.NameText:SetText(""); - f.NameText:SetTextColor(1, 1, 1, 1); - f.NameText:SetWidth(NamePlateWidth - 40); + if (isPet) then + f.NameText:SetTextColor(FBPET_NAME_COLOR[1], FBPET_NAME_COLOR[2], FBPET_NAME_COLOR[3], FBPET_NAME_COLOR[4]); + else + f.NameText:SetTextColor(1, 1, 1, 1); + end + f.NameText:SetWidth(f.nameWidthFull); + + -- Debuff-Icon rechts neben dem Namen, mit Stackzahl + f.DebuffIcon = f:CreateTexture(nil, "OVERLAY"); + f.DebuffIcon:SetWidth(FBDEBUFF_ICON_SIZE); + f.DebuffIcon:SetHeight(FBDEBUFF_ICON_SIZE); + f.DebuffIcon:SetPoint("TOPLEFT", f, "TOPLEFT", nameX + f.nameWidthIcon + 3, -7); + f.DebuffIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93); + f.DebuffIcon:Hide(); + f.DebuffCount = f:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall"); + f.DebuffCount:SetPoint("BOTTOMRIGHT", f.DebuffIcon, "BOTTOMRIGHT", 3, -2); + f.DebuffCount:SetText(""); + f.DebuffCount:Hide(); + + -- Sichtlinien-Abzeichen am linken Rand + f.LOSIcon = f:CreateTexture(nil, "OVERLAY"); + f.LOSIcon:SetWidth(FBLOS_ICON_SIZE); + f.LOSIcon:SetHeight(FBLOS_ICON_SIZE); + f.LOSIcon:SetPoint("RIGHT", f, "LEFT", FBLOS_ICON_X, FBLOS_ICON_Y); + f.LOSIcon:SetTexture(FBLOS_ICON); + f.LOSIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93); + f.LOSIcon:Hide(); f.HPText = f:CreateFontString(nil, "OVERLAY", "NumberFontNormalYellow"); f.HPText:SetPoint("RIGHT", -5, 0); @@ -915,7 +1482,7 @@ function FBHealBoxCreateFrame(FrameName,ParentFrame,FrameTexture,FrameWidth,Fram f.HPText:SetTextColor(1, 1, 1, 1); f.HealthBar = CreateFrame("STATUSBAR", nil, f, "TextStatusBar"); - f.HealthBar:SetWidth(NamePlateWidth - 5); + f.HealthBar:SetWidth(barW); f.HealthBar:SetHeight(NamePlateHeight - 5); f.HealthBar:SetPoint("TOPLEFT", 2, -3); f.HealthBar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar"); @@ -926,7 +1493,7 @@ function FBHealBoxCreateFrame(FrameName,ParentFrame,FrameTexture,FrameWidth,Fram -- Absorb-Schild als halbtransparentes "Pseudoleben" hinter dem HP-Balken f.ShieldBar = CreateFrame("STATUSBAR", nil, f, "TextStatusBar"); - f.ShieldBar:SetWidth(NamePlateWidth - 5); + f.ShieldBar:SetWidth(barW); f.ShieldBar:SetHeight(NamePlateHeight - 5); f.ShieldBar:SetPoint("TOPLEFT", 2, -3); f.ShieldBar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar"); @@ -937,7 +1504,7 @@ function FBHealBoxCreateFrame(FrameName,ParentFrame,FrameTexture,FrameWidth,Fram -- Eingehende Heilung (Direktheilung + HoT-Restticks) f.IncHealBar = CreateFrame("STATUSBAR", nil, f, "TextStatusBar"); - f.IncHealBar:SetWidth(NamePlateWidth - 5); + f.IncHealBar:SetWidth(barW); f.IncHealBar:SetHeight(NamePlateHeight - 5); f.IncHealBar:SetPoint("TOPLEFT", 2, -3); f.IncHealBar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar"); @@ -946,28 +1513,93 @@ function FBHealBoxCreateFrame(FrameName,ParentFrame,FrameTexture,FrameWidth,Fram f.IncHealBar:SetStatusBarColor(0.4, 1, 0.4, 0.5); f.IncHealBar:Show(); - -- Reihenfolge: HP oben (deckend), darunter Schild, darunter Heilvorhersage + -- Manabalken: FBMANA_BAR_HEIGHT px am unteren Rand des Lebensbalkens, + -- "Balken im Balken". Nur sichtbar bei Einheiten mit Mana. + f.ManaBar = CreateFrame("STATUSBAR", nil, f, "TextStatusBar"); + f.ManaBar:SetWidth(barW); + f.ManaBar:SetHeight(FBMANA_BAR_HEIGHT); + f.ManaBar:SetPoint("BOTTOMLEFT", f.HealthBar, "BOTTOMLEFT", 0, 0); + f.ManaBar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar"); + f.ManaBar:SetMinMaxValues(0, 1); + f.ManaBar:SetValue(0); + f.ManaBar:SetStatusBarColor(FBMANA_BAR_COLOR[1], FBMANA_BAR_COLOR[2], FBMANA_BAR_COLOR[3], FBMANA_BAR_COLOR[4]); + -- dunkler Streifen dahinter, damit fehlendes Mana lesbar bleibt + f.ManaBar.bg = f.ManaBar:CreateTexture(nil, "BACKGROUND"); + f.ManaBar.bg:SetAllPoints(f.ManaBar); + f.ManaBar.bg:SetTexture(0, 0, 0, FBMANA_BG_ALPHA); + f.ManaBar:Hide(); + + -- Reihenfolge: Mana ganz oben, HP 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); + f:SetScript("OnMouseDown", function() FBHealBox_PlateMouseDown(f); end); + f:SetScript("OnMouseUp", function() FBHealBox_PlateMouseUp(f); end); return f; end +-- Zauber castString auf das Ziel des Buttons wirken (Links- oder Rechtsklick) +function FBHealBox_CastOn(button, castString) + if (not castString) then return; end + local castTarget = button.TargetUnit or "player"; + + -- Geisterspieler: nichts casten, nur Hinweis + if (FBTest_Ghost(castTarget)) then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..FBT("TEST_CLICK")); + return; + end + + if (castTarget == "player") then + -- Ziel VOR dem Cast merken: SPELLCAST_START feuert sofort + FBPredict_NoteCast(castString, UnitName("player")); + CastSpellByName(castString, 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(castString, UnitName(castTarget)); + CastSpellByName(castString, 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(castString, UnitName(castTarget)); + CastSpellByName(castString); + if (not targetWasSame) then + if (hadTarget) then TargetLastTarget(); else ClearTarget(); end + end + end +end + function FBHealBoxCreateButton(FBButtonName, FBParentFrame, xoffset, yoffset, texture, tooltiptext, targetUnit, spellID) - if (FBButtonName == "") then - FBButtonName = "Button"..random(10); + if (not FBButtonName) or (FBButtonName == "") then + FBButtonName = nil; -- namenlos statt "Button"..random(): keine Kollisionen mit fremden Globals end local button = CreateFrame("Button", FBButtonName, FBParentFrame); button:SetPoint("LEFT", FBParentFrame, "RIGHT", xoffset, yoffset); - button.icon = button:CreateTexture("icon", "BACKGROUND"); + button.icon = button:CreateTexture(nil, "BACKGROUND"); button.icon:SetAllPoints(); + -- Eck-Icon fuer den Rechtsklick-Zauber (nur sichtbar, wenn belegt und aktiviert) + button.subIcon = button:CreateTexture(nil, "OVERLAY"); + button.subIcon:SetWidth(12); + button.subIcon:SetHeight(12); + button.subIcon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -1, 1); + button.subIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93); + button.subIcon:Hide(); + local highlight = button:CreateTexture(nil, "HIGHLIGHT"); highlight:SetAllPoints(); highlight:SetBlendMode("ADD"); @@ -990,12 +1622,16 @@ function FBHealBoxCreateButton(FBButtonName, FBParentFrame, xoffset, yoffset, te button:SetScript("OnEnter", function() GameTooltip:SetOwner(button, "ANCHOR_RIGHT", -30, 5); - if (tooltiptext == nil or button.id == nil) then + if (button.spellName == 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); + local tname = FBUnitName(this.TargetUnit) or "?"; + GameTooltip:AddLine(FBADDON_NAME.." "..FBT("TT_TARGET")..": |cFF00FF00"..tname, 1, 1, 1); + end + if (button.spellNameR) then + GameTooltip:AddLine(FBT("TT_RIGHT")..": |cFFFFFFFF"..button.spellNameR, 0.6, 0.8, 1); end GameTooltip:Show(); end); @@ -1007,37 +1643,9 @@ function FBHealBoxCreateButton(FBButtonName, FBParentFrame, xoffset, yoffset, te 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 + local castString = button.spellName; + if (arg1 == "RightButton") then castString = button.spellNameR; end + FBHealBox_CastOn(button, castString); end); button:RegisterEvent("SPELL_UPDATE_USABLE"); @@ -1046,68 +1654,336 @@ function FBHealBoxCreateButton(FBButtonName, FBParentFrame, xoffset, yoffset, te 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 }; + local bg = "Interface/DialogFrame/UI-DialogBox-Background"; + -- Spieler-Plaketten FBHealBox1..5, Begleiter-Plaketten FBHealBoxPet1..5. + -- Alle haengen an FBHealBox1: die wird verschoben, der Rest folgt. + FBHealBox1 = FBHealBoxCreateFrame("FBHealBox1", UIParent, bg, NamePlateWidth, NamePlateHeight, 1, "player", false); + FBPartyFrame[1] = FBHealBox1; + for p = 2, FBSlotCount do + local name; + if (FBSlotIsPet[p]) then name = "FBHealBoxPet"..(p - 5); else name = "FBHealBox"..p; end + FBPartyFrame[p] = FBHealBoxCreateFrame(name, FBHealBox1, bg, NamePlateWidth, NamePlateHeight, 1, FBPartyUnit[p], FBSlotIsPet[p]); + end + FBHealBox2 = FBPartyFrame[2]; FBHealBox3 = FBPartyFrame[3]; + FBHealBox4 = FBPartyFrame[4]; FBHealBox5 = FBPartyFrame[5]; + FBHealBoxPet1 = FBPartyFrame[6]; FBHealBoxPet2 = FBPartyFrame[7]; FBHealBoxPet3 = FBPartyFrame[8]; + FBHealBoxPet4 = FBPartyFrame[9]; FBHealBoxPet5 = FBPartyFrame[10]; 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 + for p = 1, FBSlotCount do + local unit = FBPartyUnit[p]; + if (FBUnitExists(unit)) then FBHealBox_UpdateUnit(unit, FBPartyFrame[p]); end + end +end + +-- Ist dieser Slot gerade anzuzeigen? (Begleiter nur mit ShowPets) +function FBSlotActive(p) + if (p == 1) then return true; end + if (FBSlotIsPet[p] and HealBox.ShowPets ~= 1) then return false; end + return FBUnitExists(FBPartyUnit[p]); +end + +-- Plaketten senkrecht anordnen: in FBLayoutOrder (Besitzer, darunter sein +-- Begleiter), nur sichtbare Plaketten zaehlen, Abstand = HealBox.RowSpacing. +-- Im Party-Frame-Modus haengen die Slots an Blizzards Frames, dann nichts tun. +function FBHealBox_Layout() + if (not FBHealBox1) or (HealBox.AttachMode == 1) then return; end + local gap = HealBox.RowSpacing or 4; + local prev = FBHealBox1; + for _, p in ipairs(FBLayoutOrder) do + if (p ~= 1) then + local f = FBPartyFrame[p]; + f:ClearAllPoints(); + -- Begleiter ruecken ein, der naechste Spieler rueckt wieder aus + local xShift = (f.indent or 0) - (prev.indent or 0); + f:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", xShift, -gap); + if (f:IsShown()) then prev = f; end + end + end end function FBUpdateNames() - FBHealBox1:Hide(); - FBHealBox2:Hide(); - FBHealBox3:Hide(); - FBHealBox4:Hide(); - FBHealBox5:Hide(); - FBPlayerName = strupper(GetUnitName("player")); + if (not FBHealBox1) then return; end + for p = 1, FBSlotCount do FBPartyFrame[p]:Hide(); end if (HealBox.Active == 1) then FBHealBox1:Show(); end + FBHealBox1.NameText:SetText(strupper(FBUnitName("player") or "")); + FBHealBox_ApplyNameColor("player", FBHealBox1); - if(UnitExists("party1")) then - FBParty1Name = strupper(GetUnitName("party1")); - FBHealBox2:Show(); + for p = 2, FBSlotCount do + local f = FBPartyFrame[p]; + if (FBSlotActive(p)) then + f.NameText:SetText(strupper(FBUnitName(FBPartyUnit[p]) or "")); + FBHealBox_ApplyNameColor(FBPartyUnit[p], f); + f:Show(); + end end - if(UnitExists("party2")) then - FBParty2Name = strupper(GetUnitName("party2")); - FBHealBox3.HealthBar:SetMinMaxValues(0,UnitHealth("party2")); - FBHealBox3:Show(); + FBHealBox_Layout(); + FBHealBox_CheckAllWatchBuffs(); + FBHealBox_CheckRangeAll(); + FBHealBox_CheckLOSAll(); + FBHealBox_RefreshAllBars(); +end + +-- Name in Klassenfarbe (Spieler) bzw. Pet-Farbe (Begleiter) +function FBHealBox_ApplyNameColor(unit, f) + if (not f) or (not f.NameText) then return; end + if (f.isPet) then + f.NameText:SetTextColor(FBPET_NAME_COLOR[1], FBPET_NAME_COLOR[2], FBPET_NAME_COLOR[3], FBPET_NAME_COLOR[4]); + return; end - - if(UnitExists("party3")) then - FBParty3Name = strupper(GetUnitName("party3")); - FBHealBox4.HealthBar:SetMinMaxValues(0,UnitHealth("party3")); - FBHealBox4:Show(); + local c = nil; + if (HealBox.ClassColors == 1) then c = FBClassColor(FBUnitClassToken(unit)); end + if (c) then + f.NameText:SetTextColor(c.r, c.g, c.b, 1); + else + f.NameText:SetTextColor(1, 1, 1, 1); end - - if(UnitExists("party4")) then - FBParty4Name = strupper(GetUnitName("party4")); - FBHealBox5.HealthBar:SetMinMaxValues(0,UnitHealth("party4")); - FBHealBox5:Show(); +end + +-- Alle Namen neu einfaerben (Schalter umgelegt) +function FBHealBox_ApplyAllNameColors() + for p = 1, FBSlotCount do + if (FBPartyFrame[p]) then FBHealBox_ApplyNameColor(FBPartyUnit[p], FBPartyFrame[p]); end end +end + +-- ========================================================================== +-- [ Buff-Wache ] +-- +-- Fehlt der gewaehlte Buff (oder seine Gruppenversion), wird der Rahmen der +-- Plakette orange. Geprueft wird bei UNIT_AURA und nach Gruppenwechseln: +-- zuerst schnell ueber die Buff-Textur (Zauberbuch-Icon), und nur wenn die +-- nicht passt ueber den Buff-Namen aus dem Tooltip. So faellt auch die +-- Gruppenversion eines anderen Heilers auf, deren Textur wir nicht kennen. +-- ========================================================================== + +function FBHealBox_SetWatchBuff(spellName) + HealBox.WatchBuff = spellName; + FBHealBox_UpdateBuffWatchLabel(); + FBHealBox_CheckAllWatchBuffs(); +end + +function FBHealBox_UpdateBuffWatchLabel() + if (not FBBuffWatchBtn) then return; end + local shown = HealBox.WatchBuff or FBT("BUFFWATCH_NONE"); + FBBuffWatchBtn.text:SetText(FBT("BUFFWATCH") .. ": |cFFFFFFFF" .. shown); + local sd = HealBox.WatchBuff and FBBuffSpells[HealBox.WatchBuff]; + if (sd and sd.icon) then + FBBuffWatchBtn.icon:SetTexture(sd.icon); + else + FBBuffWatchBtn.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark"); + end +end + +function FBHealBox_UpdatePlateActionLabels() + if (FBPlateLeftBtn) then + FBPlateLeftBtn.text:SetText(FBT("PLATE_LEFT") .. ": |cFFFFFFFF" .. FBT(FBPlateActionName[HealBox.PlateLeft or "target"])); + end + if (FBPlateRightBtn) then + FBPlateRightBtn.text:SetText(FBT("PLATE_RIGHT") .. ": |cFFFFFFFF" .. FBT(FBPlateActionName[HealBox.PlateRight or "target"])); + end +end + +-- Name eines Buffs ueber den Scan-Tooltip lesen +function FBHealBox_BuffName(unit, index) + if (not FBPredictTip) or (not FBPredictTip.SetUnitBuff) then return nil; end + FBPredictTip:SetOwner(UIParent, "ANCHOR_NONE"); + FBPredictTip:ClearLines(); + FBPredictTip:SetUnitBuff(unit, index); + local fs = getglobal("FBHealBoxScanTipTextLeft1"); + if (fs) then return fs:GetText(); end + return nil; +end + +-- true, wenn die Einheit den ueberwachten Buff (oder die Gruppenversion) traegt +function FBHealBox_HasWatchBuff(unit) + local want = HealBox.WatchBuff; + if (not want) then return true; end + local g = FBTest_Ghost(unit); + if (g) then return (not g.buffMissing); end - FBHealBox1.NameText:SetText(FBPlayerName); - FBHealBox2.NameText:SetText(FBParty1Name); - FBHealBox3.NameText:SetText(FBParty2Name); - FBHealBox4.NameText:SetText(FBParty3Name); - FBHealBox5.NameText:SetText(FBParty4Name); + local alt = FBBuffAlternates[want]; + local wantTex = FBBuffSpells[want] and strupper(FBBuffSpells[want].icon or ""); + local altTex = alt and FBBuffSpells[alt] and strupper(FBBuffSpells[alt].icon or ""); + + local i = 1; + while (i <= 32) do + local tex = UnitBuff(unit, i); + if (not tex) then break; end + tex = strupper(tex); + if (wantTex and tex == wantTex) then return true; end + if (altTex and tex == altTex) then return true; end + i = i + 1; + end + -- Textur nicht dabei: Namen pruefen (faengt fremde Gruppenversionen) + i = 1; + while (i <= 32) do + local tex = UnitBuff(unit, i); + if (not tex) then break; end + local name = FBHealBox_BuffName(unit, i); + if (name and (name == want or name == alt)) then return true; end + i = i + 1; + end + return false; +end + +function FBHealBox_CheckWatchBuff(unit, f) + if (not f) or (not f.SetBackdropBorderColor) then return; end + local missing = false; + -- Begleiter nur, wenn ausdruecklich eingeschaltet + local skipPet = (f.isPet and HealBox.BuffWatchPets ~= 1); + if (HealBox.WatchBuff and FBUnitExists(unit) and not f.plateHidden and not skipPet) then + missing = (not FBHealBox_HasWatchBuff(unit)); + end + if (f.buffMissing ~= missing) then + f.buffMissing = missing; + local c = FBBUFF_NORMAL_COLOR; + if (missing) then c = FBBUFF_MISSING_COLOR; end + f:SetBackdropBorderColor(c[1], c[2], c[3], c[4]); + end +end + +function FBHealBox_CheckAllWatchBuffs() + for p = 1, FBSlotCount do + if (FBPartyFrame[p]) then FBHealBox_CheckWatchBuff(FBPartyUnit[p], FBPartyFrame[p]); end + end +end + +-- ========================================================================== +-- [ Sichtlinie ] +-- +-- Die 1.12-API kennt keine LoS-Abfrage. Zwei Wege: +-- 1) UnitXP SP3 (Client-Mod): UnitXP("inSight", "player", unit), live. +-- 2) Sonst merkt sich das Addon die Fehlermeldung SPELL_FAILED_LINE_OF_SIGHT +-- fuer das Ziel des letzten Heilversuchs (FBLOS_TIMEOUT Sekunden) und +-- loescht sie, sobald ein Cast auf die Einheit startet oder eine eigene +-- Heilung dort ankommt. +-- ========================================================================== + +FBLOSFlags = {}; -- [Name] = Ablaufzeit der Markierung (Fallback-Weg) + +function FBLOS_HasUnitXP() + return (UnitXP ~= nil); +end + +-- UnitXP sicher abfragen: true / false, nil wenn nicht verfuegbar +function FBLOS_QueryUnitXP(unit) + if (not UnitXP) then return nil; end + local ok, res = pcall(UnitXP, "inSight", "player", unit); + if (not ok) then return nil; end + return res; +end + +-- true, wenn die Einheit als ausserhalb der Sichtlinie gilt +function FBUnitLOSBlocked(unit) + if (unit == "player") then return false; end + local g = FBTest_Ghost(unit); + if (g) then return (g.los == true); end + if (not UnitExists(unit)) then return false; end + + local live = FBLOS_QueryUnitXP(unit); + if (live ~= nil) then return (not live); end + + local name = UnitName(unit); + local until_ = name and FBLOSFlags[name]; + if (until_ and GetTime() < until_) then return true; end + if (until_) then FBLOSFlags[name] = nil; end + return false; +end + +-- Fehlermeldung "nicht in Sichtlinie": Ziel des letzten Heilversuchs markieren +function FBLOS_OnError(msg) + if (not msg) then return; end + local losText = SPELL_FAILED_LINE_OF_SIGHT or "Target not in line of sight"; + if (msg ~= losText) then return; end + local name = FBPredict_ResolveTarget(); + if (not name) then return; end + FBLOSFlags[name] = GetTime() + FBLOS_TIMEOUT; + FBHealBox_CheckLOSAll(); +end + +function FBLOS_Clear(name) + if (name and FBLOSFlags[name]) then + FBLOSFlags[name] = nil; + FBHealBox_CheckLOSAll(); + end +end + +function FBHealBox_CheckLOSAll() + for p = 1, FBSlotCount do + local f = FBPartyFrame[p]; + if (f and f.LOSIcon) then + local blocked = false; + if (HealBox.LOSIcon == 1 and f:IsShown() and not f.plateHidden) then + blocked = FBUnitLOSBlocked(FBPartyUnit[p]); + end + if (f.losBlocked ~= blocked) then + f.losBlocked = blocked; + if (blocked) then f.LOSIcon:Show(); else f.LOSIcon:Hide(); end + end + end + end +end + +-- ========================================================================== +-- [ Reichweiten-Fading ] +-- +-- Reichweite ist kein Event, also alle FBRANGE_INTERVAL Sekunden pruefen: +-- IsSpellInRange mit dem ersten belegten Button-Zauber; ohne Zauber +-- CheckInteractDistance(4) = 28 Meter. Ausser Reichweite -> ganze Plakette +-- samt Buttons auf FBRANGE_ALPHA. +-- ========================================================================== + +FBRangeAccum = 0; + +function FBHealBox_RangeSpellID() + for i = 1, MaxButtonCount do + if (FBActiveSpellIDs[i]) then return FBActiveSpellIDs[i]; end + end + return nil; +end + +function FBHealBox_UnitInRange(unit) + if (unit == "player") then return true; end + local g = FBTest_Ghost(unit); + if (g) then return (not g.outOfRange); end + if (not UnitExists(unit)) then return true; end + local id = FBHealBox_RangeSpellID(); + if (id) then + local r = IsSpellInRange(id, BOOKTYPE_SPELL, unit); + if (r == 0) then return false; end + if (r == 1) then return true; end + end + if (CheckInteractDistance) then + return (CheckInteractDistance(unit, 4) ~= nil); + end + return true; +end + +function FBHealBox_CheckRangeAll() + for p = 1, FBSlotCount do + local f = FBPartyFrame[p]; + if (f) then + local faded = false; + if (HealBox.RangeFade == 1 and f:IsShown()) then + faded = (not FBHealBox_UnitInRange(FBPartyUnit[p])); + end + if (f.rangeFaded ~= faded) then + f.rangeFaded = faded; + if (faded) then f:SetAlpha(FBRANGE_ALPHA); else f:SetAlpha(1); end + end + end + end 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. +-- 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 @@ -1123,10 +1999,21 @@ function FBHealBox_ApplyLocale() -- belegte Buttons zeigen den Zaubernamen, der bleibt wie er ist if (not FBDropDownButton[i]) then b.text:SetText(FBT("SELECT_SPELL")); end end + local r = FBSpellBtnsR[i]; + if (r and not FBDropDownButtonR[i]) then r.text:SetText(FBT("SELECT_SPELL")); end + end + if (panel.tabButtons) then + panel.tabButtons[1].text:SetText(FBT("TAB_BUTTONS")); + panel.tabButtons[2].text:SetText(FBT("TAB_GENERAL")); + end + if (panel.colLeft) then + panel.colLeft:SetText(FBT("COL_LEFT")); + panel.colRight:SetText(FBT("COL_RIGHT")); end FBUpdateButtonSliderText(); FBUpdateScaleSliderText(); + FBUpdateSpacingSliderText(); if (ScaleSlider) then getglobal(ScaleSlider:GetName() .. "Low"):SetText(FBT("SMALL")); getglobal(ScaleSlider:GetName() .. "High"):SetText(FBT("LARGE")); @@ -1140,17 +2027,151 @@ function FBHealBox_ApplyLocale() HealCommCheck.Text:SetText(FBT("COMM")); HealCommCheck.tooltipText = FBT("COMM_TIP"); end + if (ManaBarCheck) then + ManaBarCheck.Text:SetText(FBT("MANABAR")); + ManaBarCheck.tooltipText = FBT("MANABAR_TIP"); + end + if (ShowPetsCheck) then + ShowPetsCheck.Text:SetText(FBT("SHOWPETS")); + ShowPetsCheck.tooltipText = FBT("SHOWPETS_TIP"); + end + if (TestModeCheck) then + TestModeCheck.Text:SetText(FBT("TESTMODE")); + TestModeCheck.tooltipText = FBT("TESTMODE_TIP"); + end + if (ClassColorsCheck) then + ClassColorsCheck.Text:SetText(FBT("CLASSCOLORS")); + ClassColorsCheck.tooltipText = FBT("CLASSCOLORS_TIP"); + end + if (RangeFadeCheck) then + RangeFadeCheck.Text:SetText(FBT("RANGEFADE")); + RangeFadeCheck.tooltipText = FBT("RANGEFADE_TIP"); + end + if (DebuffIconCheck) then + DebuffIconCheck.Text:SetText(FBT("DEBUFFICON")); + DebuffIconCheck.tooltipText = FBT("DEBUFFICON_TIP"); + end + if (LOSIconCheck) then + LOSIconCheck.Text:SetText(FBT("LOSICON")); + LOSIconCheck.tooltipText = FBT("LOSICON_TIP"); + end + if (BuffWatchPetsCheck) then + BuffWatchPetsCheck.Text:SetText(FBT("BUFFWATCH_PETS")); + BuffWatchPetsCheck.tooltipText = FBT("BUFFWATCH_PETS_TIP"); + end + if (RightClickCheck) then + RightClickCheck.Text:SetText(FBT("RIGHTCLICK")); + RightClickCheck.tooltipText = FBT("RIGHTCLICK_TIP"); + end + FBHealBox_UpdateBuffWatchLabel(); + FBHealBox_UpdatePlateActionLabels(); if (FBLangBtn) then FBLangBtn.text:SetText(FBT("LANGUAGE") .. ": |cFFFFFFFF" .. FBT("LANG_NAME")); end end -- [ Options-Fenster ] -- +-- +-- Zwei Reiter: "Buttons" (Belegung der zehn Buttons, Anzahl, Rechtsklick) +-- und "Allgemein" (Skalierung, Abstaende, Schalter, Sprache, Buff-Wache). +-- Jeder Reiter ist ein eigener Frame in Panelgroesse; die Koordinaten der +-- Steuerelemente beziehen sich damit weiterhin auf die linke obere Ecke. + +FBOPT_TAB_Y = -92; -- Hoehe der Reiterleiste +FBOPT_CONTENT_Y = -128; -- erste Inhaltszeile unter der Reiterleiste + +-- Auswahlknopf mit Icon und Text (Zauberfelder, Sprache, Buff-Wache) +function FBHealBox_CreatePickButton(name, parent, x, y, width, height, iconSize) + local btn = CreateFrame("Button", name, parent); + btn:SetWidth(width); + btn:SetHeight(height); + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y); + 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(iconSize or 20); + btn.icon:SetHeight(iconSize or 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", 6, 0); + btn.text:SetPoint("RIGHT", -6, 0); + btn.text:SetJustifyH("LEFT"); + + local hl = btn:CreateTexture(nil, "HIGHLIGHT"); + hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight"); + hl:SetBlendMode("ADD"); + hl:SetAllPoints(btn); + return btn; +end + +-- Reiter-Knopf +function FBHealBox_CreateTabButton(name, parent, x, index) + local t = CreateFrame("Button", name, parent); + t:SetWidth(110); + t:SetHeight(24); + t:SetPoint("TOPLEFT", parent, "TOPLEFT", x, FBOPT_TAB_Y); + t: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 } + }); + t.text = t:CreateFontString(nil, "OVERLAY", "GameFontNormal"); + t.text:SetPoint("CENTER", 0, 0); + t.index = index; + t:SetScript("OnClick", function() + PlaySound("igMainMenuOptionCheckBoxOn"); + FBHealBox_ShowTab(this.index); + end); + return t; +end + +function FBHealBox_ShowTab(index) + if (not panel) or (not panel.tabs) then return; end + FBMenu_CloseAll(); + panel.activeTab = index; + for i, tab in ipairs(panel.tabs) do + local btn = panel.tabButtons[i]; + if (i == index) then + tab:Show(); + btn:SetBackdropColor(0.15, 0.12, 0.02, 0.9); + btn:SetBackdropBorderColor(1, 0.82, 0, 1); + btn.text:SetTextColor(1, 0.82, 0, 1); + else + tab:Hide(); + btn:SetBackdropColor(0, 0, 0, 0.8); + btn:SetBackdropBorderColor(0.5, 0.5, 0.5, 1); + btn.text:SetTextColor(0.7, 0.7, 0.7, 1); + end + end +end + +-- Rechtsklick an/aus: zweite Spalte und Spaltenkoepfe im Buttons-Reiter +function FBHealBox_ApplyRightClickLayout() + if (not panel) or (not panel.colLeft) then return; end + local on = (HealBox.RightClick == 1); + for i = 1, MaxButtonCount do + if (FBSpellBtnsR[i]) then + if (on) then FBSpellBtnsR[i]:Show(); else FBSpellBtnsR[i]:Hide(); end + end + end + if (on) then panel.colRight:Show(); else panel.colRight:Hide(); end +end + function FBHealBoxCreateAddonOptionFrame() panel = CreateFrame("FRAME", "HealBoxOptionsFrame", UIParent); panel.name = FBADDON_NAME; panel:SetWidth(460); - panel:SetHeight(620); + panel:SetHeight(600); panel:SetPoint("CENTER", UIParent, "CENTER", 0, 0); panel:SetFrameStrata("DIALOG"); panel:SetBackdrop({ @@ -1164,11 +2185,21 @@ function FBHealBoxCreateAddonOptionFrame() panel:RegisterForDrag("LeftButton"); panel:SetScript("OnDragStart", function() panel:StartMoving(); end); panel:SetScript("OnDragStop", function() panel:StopMovingOrSizing(); end); + -- Beim Schliessen (X, ESC, /fbp config) immer auch das Kaskadenmenue zu + panel:SetScript("OnHide", function() FBMenu_CloseAll(); end); panel:Hide(); + -- ESC schliesst das Fenster. Zwei Wege, weil nicht jeder Client die + -- UISpecialFrames-Liste auswertet: + -- 1) Eintrag in UISpecialFrames (Blizzards offizieller Weg) + -- 2) Vor-Hook auf ToggleGameMenu, das die ESC-Taste immer aufruft + if (UISpecialFrames) then + table.insert(UISpecialFrames, "HealBoxOptionsFrame"); + end + FBHealBox_HookEscape(); panel.CloseButton = CreateFrame("Button", "HealBoxOptionsFrameClose", panel, "UIPanelCloseButton"); panel.CloseButton:SetPoint("TOPRIGHT", -5, -5); - panel.CloseButton:SetScript("OnClick", function() FBMenu_CloseAll(); panel:Hide(); end); + panel.CloseButton:SetScript("OnClick", function() panel:Hide(); end); panel.TitleText = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge"); panel.TitleText:SetPoint("TOPLEFT", 25, -18); @@ -1192,79 +2223,108 @@ function FBHealBoxCreateAddonOptionFrame() classIcon.text:SetPoint("CENTER", 0, -45); classIcon.text:SetTextColor(1, 1, 0.2, 1); - -- 10 Buttons für Zauber mit Rank-Auswahl + -- [ Reiter ] ------------------------------------------------------------ + panel.tabs = {}; + panel.tabButtons = {}; + for i = 1, 2 do + local tab = CreateFrame("Frame", "HealBoxOptionsTab"..i, panel); + tab:SetAllPoints(panel); + tab:Hide(); + panel.tabs[i] = tab; + panel.tabButtons[i] = FBHealBox_CreateTabButton("HealBoxOptionsTabButton"..i, panel, 25 + (i - 1) * 116, i); + end + local tabButtons = panel.tabs[1]; + local tabGeneral = panel.tabs[2]; + + -- Trennlinie unter den Reitern + panel.tabLine = panel:CreateTexture(nil, "ARTWORK"); + panel.tabLine:SetTexture(1, 0.82, 0, 0.35); + panel.tabLine:SetHeight(1); + panel.tabLine:SetPoint("TOPLEFT", panel, "TOPLEFT", 25, FBOPT_TAB_Y - 26); + panel.tabLine:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -25, FBOPT_TAB_Y - 26); + + -- ====================================================================== + -- Reiter 1: Button-Belegung + -- Zeile je Button: Label | Linksklick-Feld | Rechtsklick-Feld (optional) + -- ====================================================================== + local rowH = 32; + local xLabel = 30; + local xLeft = 92; + local xRight = 268; + local fieldW = 170; + local y0 = FBOPT_CONTENT_Y - 18; + + panel.colLeft = tabButtons:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); + panel.colLeft:SetPoint("TOPLEFT", tabButtons, "TOPLEFT", xLeft + 4, FBOPT_CONTENT_Y); + panel.colLeft:SetText(FBT("COL_LEFT")); + panel.colRight = tabButtons:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); + panel.colRight:SetPoint("TOPLEFT", tabButtons, "TOPLEFT", xRight + 4, FBOPT_CONTENT_Y); + panel.colRight:SetText(FBT("COL_RIGHT")); + 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 yPos = y0 - ((i - 1) * rowH); + local btnIndex = i; - 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"); + local btn = FBHealBox_CreatePickButton("FBHealBoxBtn"..i, tabButtons, xLeft, yPos, fieldW, 28, 20); btn.text:SetText(FBT("SELECT_SPELL")); - btn.label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal"); - btn.label:SetPoint("BOTTOMLEFT", btn, "TOPLEFT", 2, 2); + btn.label:SetPoint("RIGHT", btn, "LEFT", -8, 0); + btn.label:SetJustifyH("RIGHT"); 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); - + btn:SetScript("OnClick", function() + PlaySound("igMainMenuOptionCheckBoxOn"); + FBMenu_OpenSpellMenu(btnIndex, btn, "L"); + end); FBSpellBtns[i] = btn; + + local btnR = FBHealBox_CreatePickButton("FBHealBoxBtnR"..i, tabButtons, xRight, yPos, fieldW, 28, 20); + btnR.text:SetText(FBT("SELECT_SPELL")); + btnR:SetScript("OnClick", function() + PlaySound("igMainMenuOptionCheckBoxOn"); + FBMenu_OpenSpellMenu(btnIndex, btnR, "R"); + end); + btnR:Hide(); + FBSpellBtnsR[i] = btnR; end - MaxButtonSlider = CreateFrame("Slider", "MaxButtonSlider", panel, "OptionsSliderTemplate"); - MaxButtonSlider:SetWidth(128); + local yBelow = y0 - (MaxButtonCount * rowH) - 30; -- unter der letzten Zeile + + MaxButtonSlider = CreateFrame("Slider", "MaxButtonSlider", tabButtons, "OptionsSliderTemplate"); + MaxButtonSlider:SetWidth(fieldW); MaxButtonSlider:SetHeight(16); - MaxButtonSlider:SetPoint("TOPLEFT", 75, -460); + MaxButtonSlider:SetPoint("TOPLEFT", xLeft, yBelow); MaxButtonSlider:SetMinMaxValues(0, MaxButtonCount); MaxButtonSlider:SetValueStep(1); MaxButtonSlider:SetValue(HealBox.MaxButtons); - MaxButtonSlider.Text = MaxButtonSlider:CreateFontString(nil, "BACKGROUND", "GameFontNormalLarge"); - MaxButtonSlider.Text:SetPoint("CENTER", 0, 17); + MaxButtonSlider.Text = MaxButtonSlider:CreateFontString(nil, "BACKGROUND", "GameFontNormal"); + MaxButtonSlider.Text:SetPoint("CENTER", 0, 15); 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); + -- Rechtsklick-Zweitzauber: bewusst nur hier einschaltbar, Standard aus + RightClickCheck = FBHealBox_CreateCheck("FBHealBoxRightClickCheck", tabButtons, xRight - 6, yBelow + 6, "RIGHTCLICK", "RIGHTCLICK_TIP", function() + HealBox.RightClick = RightClickCheck:GetChecked() and 1 or 0; + FBHealBox_ApplyRightClickLayout(); + FBHealBoxButtonsChanged(); + end); + RightClickCheck:SetChecked(nil); + + -- ====================================================================== + -- Reiter 2: Allgemeine Einstellungen + -- ====================================================================== + local gy = FBOPT_CONTENT_Y - 22; + + ScaleSlider = CreateFrame("Slider", "ScaleSlider", tabGeneral, "OptionsSliderTemplate"); + ScaleSlider:SetWidth(128); ScaleSlider:SetHeight(16); - ScaleSlider:SetPoint("TOPLEFT", 260, -460); + ScaleSlider:SetPoint("TOPLEFT", 75, gy); 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); + ScaleSlider.Text = ScaleSlider:CreateFontString(nil, "BACKGROUND", "GameFontNormal"); + ScaleSlider.Text:SetPoint("CENTER", 0, 15); FBUpdateScaleSliderText(); getglobal(ScaleSlider:GetName() .. "Low"):SetText(FBT("SMALL")); getglobal(ScaleSlider:GetName() .. "High"):SetText(FBT("LARGE")); @@ -1274,153 +2334,297 @@ function FBHealBoxCreateAddonOptionFrame() 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(); + -- Abstand der Buttons zueinander (0..20 px) + ButtonSpacingSlider = CreateFrame("Slider", "FBButtonSpacingSlider", tabGeneral, "OptionsSliderTemplate"); + ButtonSpacingSlider:SetWidth(128); + ButtonSpacingSlider:SetHeight(16); + ButtonSpacingSlider:SetPoint("TOPLEFT", 260, gy); + ButtonSpacingSlider:SetMinMaxValues(0, 20); + ButtonSpacingSlider:SetValueStep(1); + ButtonSpacingSlider:SetValue(HealBox.ButtonSpacing or 2); + ButtonSpacingSlider.Text = ButtonSpacingSlider:CreateFontString(nil, "BACKGROUND", "GameFontNormal"); + ButtonSpacingSlider.Text:SetPoint("CENTER", 0, 15); + getglobal(ButtonSpacingSlider:GetName() .. "Low"):SetText("0"); + getglobal(ButtonSpacingSlider:GetName() .. "High"):SetText("20"); + ButtonSpacingSlider:SetScript("OnValueChanged", function() + HealBox.ButtonSpacing = math.floor(ButtonSpacingSlider:GetValue() + 0.5); + FBUpdateSpacingSliderText(); + FBHealBox_ApplyButtonSpacing(); end); - AttachModeCheck:SetScript("OnLeave", function() GameTooltip:Hide(); end); - AttachModeCheck:SetScript("OnClick", function() - HealBox.AttachMode = AttachModeCheck:GetChecked(); + + -- Abstand der Plaketten zueinander (0..20 px) + RowSpacingSlider = CreateFrame("Slider", "FBRowSpacingSlider", tabGeneral, "OptionsSliderTemplate"); + RowSpacingSlider:SetWidth(128); + RowSpacingSlider:SetHeight(16); + RowSpacingSlider:SetPoint("TOPLEFT", 75, gy - 50); + RowSpacingSlider:SetMinMaxValues(0, 20); + RowSpacingSlider:SetValueStep(1); + RowSpacingSlider:SetValue(HealBox.RowSpacing or 4); + RowSpacingSlider.Text = RowSpacingSlider:CreateFontString(nil, "BACKGROUND", "GameFontNormal"); + RowSpacingSlider.Text:SetPoint("CENTER", 0, 15); + getglobal(RowSpacingSlider:GetName() .. "Low"):SetText("0"); + getglobal(RowSpacingSlider:GetName() .. "High"):SetText("20"); + RowSpacingSlider:SetScript("OnValueChanged", function() + HealBox.RowSpacing = math.floor(RowSpacingSlider:GetValue() + 0.5); + FBUpdateSpacingSliderText(); + FBHealBox_Layout(); + end); + FBUpdateSpacingSliderText(); + + -- [ Schalter ] ---------------------------------------------------------- + local cy = gy - 95; + AttachModeCheck = FBHealBox_CreateCheck("$parentCheckButton", tabGeneral, 40, cy, "ATTACH", "ATTACH_TIP", function() + HealBox.AttachMode = AttachModeCheck:GetChecked() and 1 or 0; 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); - + HealCommCheck = FBHealBox_CreateCheck("FBHealBoxHealCommCheck", tabGeneral, 250, cy, "COMM", "COMM_TIP", function() + HealBox.HealComm = HealCommCheck:GetChecked() and 1 or 0; + if (HealBox.HealComm == 0) then + FBCommHeals = {}; + FBHealBox_RefreshAllBars(); + end + end); + HealCommCheck:SetChecked(1); + + ManaBarCheck = FBHealBox_CreateCheck("FBHealBoxManaBarCheck", tabGeneral, 40, cy - 30, "MANABAR", "MANABAR_TIP", function() + HealBox.ManaBar = ManaBarCheck:GetChecked() and 1 or 0; + FBHealBox_RefreshAllBars(); + end); + ManaBarCheck:SetChecked(1); + + ShowPetsCheck = FBHealBox_CreateCheck("FBHealBoxShowPetsCheck", tabGeneral, 250, cy - 30, "SHOWPETS", "SHOWPETS_TIP", function() + HealBox.ShowPets = ShowPetsCheck:GetChecked() and 1 or 0; + FBUpdateNames(); + end); + ShowPetsCheck:SetChecked(1); + + ClassColorsCheck = FBHealBox_CreateCheck("FBHealBoxClassColorsCheck", tabGeneral, 40, cy - 60, "CLASSCOLORS", "CLASSCOLORS_TIP", function() + HealBox.ClassColors = ClassColorsCheck:GetChecked() and 1 or 0; + FBHealBox_ApplyAllNameColors(); + end); + ClassColorsCheck:SetChecked(1); + + RangeFadeCheck = FBHealBox_CreateCheck("FBHealBoxRangeFadeCheck", tabGeneral, 250, cy - 60, "RANGEFADE", "RANGEFADE_TIP", function() + HealBox.RangeFade = RangeFadeCheck:GetChecked() and 1 or 0; + FBHealBox_CheckRangeAll(); + end); + RangeFadeCheck:SetChecked(1); + + DebuffIconCheck = FBHealBox_CreateCheck("FBHealBoxDebuffIconCheck", tabGeneral, 40, cy - 90, "DEBUFFICON", "DEBUFFICON_TIP", function() + HealBox.DebuffIcon = DebuffIconCheck:GetChecked() and 1 or 0; + FBHealBox_RefreshAllBars(); + end); + DebuffIconCheck:SetChecked(1); + + LOSIconCheck = FBHealBox_CreateCheck("FBHealBoxLOSIconCheck", tabGeneral, 250, cy - 90, "LOSICON", "LOSICON_TIP", function() + HealBox.LOSIcon = LOSIconCheck:GetChecked() and 1 or 0; + FBHealBox_CheckLOSAll(); + end); + LOSIconCheck:SetChecked(1); + + TestModeCheck = FBHealBox_CreateCheck("FBHealBoxTestModeCheck", tabGeneral, 40, cy - 120, "TESTMODE", "TESTMODE_TIP", function() + FBTest_Set(TestModeCheck:GetChecked()); + end); + TestModeCheck:SetChecked(nil); + + BuffWatchPetsCheck = FBHealBox_CreateCheck("FBHealBoxBuffWatchPetsCheck", tabGeneral, 250, cy - 120, "BUFFWATCH_PETS", "BUFFWATCH_PETS_TIP", function() + HealBox.BuffWatchPets = BuffWatchPetsCheck:GetChecked() and 1 or 0; + FBHealBox_CheckAllWatchBuffs(); + end); + BuffWatchPetsCheck:SetChecked(nil); + + -- [ Sprache und Buff-Wache ] ------------------------------------------- + local py = cy - 170; + FBLangBtn = FBHealBox_CreatePickButton("FBHealBoxLangBtn", tabGeneral, 35, py, 180, 26, 18); + FBLangBtn.icon:SetTexture("Interface\\Icons\\INV_Misc_Note_01"); + 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); + + FBBuffWatchBtn = FBHealBox_CreatePickButton("FBHealBoxBuffWatchBtn", tabGeneral, 245, py, 180, 26, 18); + FBBuffWatchBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT"); + GameTooltip:SetText(FBT("BUFFWATCH")); + GameTooltip:AddLine(FBT("BUFFWATCH_TIP"), 1, 1, 1, true); + GameTooltip:Show(); + end); + FBBuffWatchBtn:SetScript("OnLeave", function() GameTooltip:Hide(); end); + FBBuffWatchBtn:SetScript("OnClick", function() + PlaySound("igMainMenuOptionCheckBoxOn"); + FBMenu_OpenBuffMenu(FBBuffWatchBtn); + end); + FBHealBox_UpdateBuffWatchLabel(); + + -- [ Klick auf die Plakette ] -------------------------------------------- + FBPlateLeftBtn = FBHealBox_CreatePickButton("FBHealBoxPlateLeftBtn", tabGeneral, 35, py - 36, 180, 26, 18); + FBPlateLeftBtn.icon:SetTexture("Interface\\Icons\\Ability_Hunter_SniperShot"); + FBPlateLeftBtn.side = "L"; + FBPlateRightBtn = FBHealBox_CreatePickButton("FBHealBoxPlateRightBtn", tabGeneral, 245, py - 36, 180, 26, 18); + FBPlateRightBtn.icon:SetTexture("Interface\\Icons\\Ability_Hunter_SniperShot"); + FBPlateRightBtn.side = "R"; + for _, pb in ipairs({ FBPlateLeftBtn, FBPlateRightBtn }) do + pb:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT"); + if (this.side == "R") then GameTooltip:SetText(FBT("PLATE_RIGHT")); else GameTooltip:SetText(FBT("PLATE_LEFT")); end + GameTooltip:AddLine(FBT("PLATE_TIP"), 1, 1, 1, true); + GameTooltip:Show(); + end); + pb:SetScript("OnLeave", function() GameTooltip:Hide(); end); + pb:SetScript("OnClick", function() + PlaySound("igMainMenuOptionCheckBoxOn"); + FBMenu_OpenPlateActionMenu(this, this.side); + end); + end + FBHealBox_UpdatePlateActionLabels(); + 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_ShowTab(1); + FBHealBox_ApplyRightClickLayout(); 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; +-- ESC: ToggleGameMenu abfangen, solange das Optionsfenster offen ist. +-- Ist das Spielmenue selbst offen (ESC darueber gedrueckt), bleibt alles +-- beim Original; sonst schliesst ESC zuerst unser Fenster und verpufft. +FBHealBox_OrigToggleGameMenu = nil; +function FBHealBox_HookEscape() + if (FBHealBox_OrigToggleGameMenu) or (not ToggleGameMenu) then return; end + FBHealBox_OrigToggleGameMenu = ToggleGameMenu; + ToggleGameMenu = function(clicked) + local gameMenuOpen = (GameMenuFrame and GameMenuFrame:IsVisible()); + if (panel and panel:IsVisible() and not gameMenuOpen) then + panel:Hide(); + return; end + return FBHealBox_OrigToggleGameMenu(clicked); end - for p=1, 5, 1 do +end + +-- Optionsfenster auf/zu (Minimap-Button, /fbp config) +function FBHealBox_ToggleOptions() + if (not panel) then return; end + if (panel:IsVisible()) then + panel:Hide(); + else + panel:Show(); + end +end + +-- Optionsschalter mit Beschriftung und Tooltip (fuenfmal gebraucht) +function FBHealBox_CreateCheck(name, parent, x, y, labelKey, tipKey, onClick) + local c = CreateFrame("CheckButton", name, parent, "OptionsCheckButtonTemplate"); + c:SetPoint("TOPLEFT", x, y); + c.Text = c:CreateFontString(nil, "BACKGROUND", "GameFontNormal"); + c.Text:SetPoint("LEFT", c, "RIGHT", 4, 1); + c.Text:SetText(FBT(labelKey)); + c.labelKey = labelKey; + c.tooltipText = FBT(tipKey); + c:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT"); + GameTooltip:SetText(FBT(this.labelKey)); + GameTooltip:AddLine(this.tooltipText, 1, 1, 1, true); + GameTooltip:Show(); + end); + c:SetScript("OnLeave", function() GameTooltip:Hide(); end); + c:SetScript("OnClick", onClick); + return c; +end + +-- [ Namensplaketten Buttons ] -- + +-- Buttons werden genau einmal angelegt und danach nur noch umbelegt. +-- (Frueher wurden sie bei jedem SPELLS_CHANGED neu erzeugt.) +function FBHealBoxButtons() + for p = 1, FBSlotCount 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]; + if (parentFrame) then + local prevButton = nil; + for i = 1, MaxButtonCount do + if (not FBPartyTable[p][i]) then + local anchor = prevButton or parentFrame; + local name = "FBHealBoxSlot"..p.."Btn"..i; + FBPartyTable[p][i] = FBHealBoxCreateButton(name, anchor, HealBox.ButtonSpacing or 2, 0, + FBDropDownButtonIcon[i], FBDropDownButton[i], unit, FBActiveSpellIDs[i]); + end + prevButton = FBPartyTable[p][i]; + end 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(); + FBHealBox_ApplyButtonSpacing(); + FBHealBoxButtonsChanged(); +end + +function FBHealBoxButtonsChanged() + local rightOn = (HealBox.RightClick == 1); + for p = 1, FBSlotCount do + local unit = FBPartyUnit[p]; + for i = 1, MaxButtonCount do + local b = FBPartyTable[p][i]; + if (b) then + b:Hide(); + b.TargetUnit = unit; + b.spellName = FBDropDownButton[i]; + b.id = FBActiveSpellIDs[i]; + if (FBDropDownButtonIcon[i]) then + b.icon:SetTexture(FBDropDownButtonIcon[i]); + else + b.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark"); + end + -- Rechtsklick: nur wenn in den Optionen eingeschaltet + if (rightOn) then + b.spellNameR = FBDropDownButtonR[i]; + b.idR = FBActiveSpellIDsR[i]; + b:RegisterForClicks("LeftButtonUp", "RightButtonUp"); + else + b.spellNameR = nil; + b.idR = nil; + b:RegisterForClicks("LeftButtonUp"); + end + if (b.subIcon) then + if (b.spellNameR and FBDropDownButtonIconR[i]) then + b.subIcon:SetTexture(FBDropDownButtonIconR[i]); + b.subIcon:Show(); + else + b.subIcon:Hide(); + end + end + if (i <= (HealBox.MaxButtons or 0)) then b:Show(); end + end 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 +-- Buttons neu verketten: erster Button haengt an der Plakette, jeder weitere +-- am vorherigen, jeweils mit HealBox.ButtonSpacing Pixeln Luft. +function FBHealBox_ApplyButtonSpacing() + local gap = HealBox.ButtonSpacing or 2; + for p = 1, FBSlotCount do + local prev = FBPartyFrame[p]; + for i = 1, MaxButtonCount do + local b = FBPartyTable[p][i]; + if (b and prev) then + b:ClearAllPoints(); + b:SetPoint("LEFT", prev, "RIGHT", gap, 0); + prev = b; 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 @@ -1436,6 +2640,15 @@ function FBUpdateScaleSliderText() end end +function FBUpdateSpacingSliderText() + if (ButtonSpacingSlider and ButtonSpacingSlider.Text) then + ButtonSpacingSlider.Text:SetText(format(FBT("BTN_SPACING"), math.floor(ButtonSpacingSlider:GetValue() + 0.5))); + end + if (RowSpacingSlider and RowSpacingSlider.Text) then + RowSpacingSlider.Text:SetText(format(FBT("ROW_SPACING"), math.floor(RowSpacingSlider:GetValue() + 0.5))); + end +end + function MaxButtonSlider_Update() FBUpdateButtonSliderText(); HealBox.MaxButtons = MaxButtonSlider:GetValue(); @@ -1499,7 +2712,7 @@ function CreateMiniMapButton() 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 + FBHealBox_ToggleOptions(); end end end); @@ -1548,99 +2761,60 @@ function HealBoxScale(this, scale) this:SetScale(scale); end +-- Blizzard-Frame, an das ein Slot im Party-Frame-Modus gehaengt wird +function FBHealBox_BlizzAnchor(p) + if (p >= 2 and p <= 5) then return getglobal("PartyMemberFrame"..(p - 1)), 119, 6; end + if (p == 6) then return PetFrame, 4, 0; end + if (p >= 7) then return getglobal("PartyMemberFrame"..(p - 6).."PetFrame"), 4, 0; end + return nil; +end + function HealBoxAttachMode(mode) - local xoffset = 119; - local yoffset = 6; + if (not FBHealBox1) then return; end if (mode == 1) then + -- Eigene Plaketten weg: Slots 2-10 werden 1x1 Pixel gross und + -- haengen unsichtbar an Blizzards Frames, nur die Buttons bleiben. 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(); + for p = 2, FBSlotCount do + local f = FBPartyFrame[p]; + local anchor, xoff, yoff = FBHealBox_BlizzAnchor(p); + f:ClearAllPoints(); + if (anchor) then + if (p >= 6) then + f:SetPoint("LEFT", anchor, "RIGHT", xoff, yoff); + else + f:SetPoint("LEFT", anchor, "LEFT", xoff, yoff); + end + else + f:SetPoint("LEFT", FBHealBox1, "LEFT", 0, 0); + end + f:SetBackdropColor(0, 0, 0, 0.8); + f:SetWidth(1); + f:SetHeight(1); + FBHealBox_SetPlateVisible(f, false); + f:SetBackdropBorderColor(FBBUFF_NORMAL_COLOR[1], FBBUFF_NORMAL_COLOR[2], FBBUFF_NORMAL_COLOR[3], FBBUFF_NORMAL_COLOR[4]); + f.buffMissing = false; + f:SetFrameStrata("LOW"); + f:Hide(); + end 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:SetScale(HealBox.Scale or 1); + FBHealBox_RestorePosition(); + FBHealBox1:SetBackdropColor(0, 0, 0, 0.8); + FBHealBox_SetPlateVisible(FBHealBox1, true); 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(); + for p = 2, FBSlotCount do + local f = FBPartyFrame[p]; + f:SetBackdropColor(0, 0, 0, 0.8); + f:SetWidth(f.plateW or NamePlateWidth); + f:SetHeight(NamePlateHeight); + FBHealBox_SetPlateVisible(f, true); + FBHealBox_SetBarStrata(f, "BACKGROUND"); + f:Hide(); + end + FBHealBox_Layout(); end end @@ -1648,11 +2822,11 @@ 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 +-- 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 +-- 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 @@ -1670,7 +2844,7 @@ end -- ("(123 absorbed)"). -- -- Selbstkorrektur: Tooltips liefern in 1.12 nur Basiswerte ohne --- +Heilung. Der Combatlog liefert die Wahrheit - jeder beobachtete +-- +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 @@ -1703,9 +2877,9 @@ FBPredictCastTarget = nil; -- exaktes Ziel des letzten Button-Casts FBPredictCastTime = 0; FBPredictAccum = 0; -FBPredictUnits = { - ["player"] = 1, ["party1"] = 1, ["party2"] = 1, ["party3"] = 1, ["party4"] = 1, -}; +-- alle Slots inkl. Begleiter. HoTs und Schilde auf Pets werden genauso verfolgt +FBPredictUnits = {}; +for _, u in ipairs(FBPartyUnit) do FBPredictUnits[u] = 1; end -- [ Tooltip-Scanner ] ------------------------------------------------------ @@ -1883,7 +3057,7 @@ function FBPredict_FindBookID(spellName, rank) end -- rankKnown = true nur, wenn der Rang aus einem eigenen Button-Cast stammt. --- Nur dann darf der beobachtete Wert dauerhaft gemerkt werden - sonst +-- 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) @@ -1906,7 +3080,7 @@ function FBPredict_StartHoT(unitName, spellName, rank, bookID, rankKnown) end -- Nur gesicherte eigene Casts funken. Ein per Aura entdeckter HoT - -- koennte auch von einem anderen Heiler stammen - der erste eigene + -- koennte auch von einem anderen Heiler stammen. Der erste eigene -- Combatlog-Tick holt das unten nach. if (rankKnown) then FBHoTs[unitName][spellName].commSent = true; @@ -1988,7 +3162,7 @@ function FBPredict_SplitCast(castString) return castString, nil; end --- WICHTIG: direkt VOR CastSpellByName aufrufen - SPELLCAST_START feuert +-- WICHTIG: direkt VOR CastSpellByName aufrufen, denn SPELLCAST_START feuert -- sofort und braucht das Ziel bereits. function FBPredict_NoteCast(castString, targetName) if (not castString) or (not targetName) then return; end @@ -2038,6 +3212,8 @@ function FBPredict_CastStart(spellName, castMs) if (not info) or (not info.direct) then return; end local amount = FBPredict_Remembered("direct", spellName, rank) or info.direct; + FBLOS_Clear(FBPredict_ResolveTarget()); -- Cast laeuft an: Sichtlinie ist da + FBPredictDirect = { target = FBPredict_ResolveTarget(), spell = spellName, @@ -2156,7 +3332,7 @@ function FBPredict_InitPatterns() 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) + -- laufen deshalb ins Leere, genau so gewollt) FBPredictPatHealOther = FBPredict_ToPattern(HEALEDSELFOTHER, true) or "^Your (.+) heals (.+) for (%d+)%.$"; -- "Your %s heals you for %d." @@ -2190,6 +3366,7 @@ end function FBPredict_OnTick(unitName, amount, spellName) if (not unitName) or (not amount) or (not spellName) then return; end + FBLOS_Clear(unitName); local t = FBHoTs[unitName]; if (not t) or (not t[spellName]) then return; end @@ -2217,6 +3394,7 @@ end function FBPredict_OnDirectHeal(spellName, targetName, amount) if (not spellName) or (not amount) then return; end + FBLOS_Clear(targetName); local w = FBPredictWatch[spellName]; if (not w) then return; end -- faengt auch die Crit-Formulierung ab @@ -2307,6 +3485,7 @@ end FBPredictEvents = { "UNIT_AURA", "CHAT_MSG_ADDON", + "UI_ERROR_MESSAGE", -- Sichtlinien-Fehler -- eigener Cast "SPELLCAST_START", "SPELLCAST_STOP", @@ -2347,6 +3526,9 @@ FBPredictFrame:SetScript("OnEvent", function() elseif (event == "CHAT_MSG_ADDON") then FBComm_OnMessage(arg1, arg2, arg3, arg4); + elseif (event == "UI_ERROR_MESSAGE") then + FBLOS_OnError(arg1); + elseif (event == "SPELLCAST_START") then FBPredict_CastStart(arg1, tonumber(arg2)); @@ -2375,6 +3557,14 @@ FBPredictFrame:SetScript("OnUpdate", function() end); function FBPredict_OnUpdate(elapsed) + -- Reichweiten-Fading laeuft im eigenen Takt mit + FBRangeAccum = FBRangeAccum + (elapsed or 0); + if (FBRangeAccum >= FBRANGE_INTERVAL) then + FBRangeAccum = 0; + FBHealBox_CheckRangeAll(); + FBHealBox_CheckLOSAll(); + end + FBPredictAccum = FBPredictAccum + (elapsed or 0); if (FBPredictAccum < FBPREDICT_THROTTLE) then return; end FBPredictAccum = 0; @@ -2422,6 +3612,9 @@ function FBPredict_OnUpdate(elapsed) FBPredictPending = nil; end + -- Testmodus: die Geister atmen, im Takt der Vorhersage neu zeichnen + if (FBTestMode) then dirty = true; end + if (dirty) then FBHealBox_RefreshAllBars(); end end @@ -2431,7 +3624,7 @@ 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 +-- Prefix "HealComm". Wir sprechen dieselbe Sprache, ohne die Bibliothek -- einzubinden: -- -- Heal//// Direktheilung startet @@ -2442,12 +3635,12 @@ FBPredict_InitPatterns(); -- Renew|Reju|Regr/// HoT angewendet -- -- Empfaenger ignorieren die eigenen Nachrichten (Absender == man selbst), --- und HealComm legt eingehende Heilungen pro Caster ab - doppelt gesendete +-- 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. +-- also inkl. +Heilung und Talenten. Kein ItemBonusLib noetig. -- ========================================================================== FBCOMM_PREFIX = "HealComm"; @@ -2605,7 +3798,7 @@ function FBComm_OnMessage(prefix, msg, channel, sender) dirty = true; end -- HoT-Nachrichten (Renew/Reju/Regr) tragen nur Laufzeiten, keine - -- Betraege - HealComm selbst zaehlt sie ebenfalls nicht zur + -- Betraege. HealComm selbst zaehlt sie ebenfalls nicht zur -- eingehenden Heilung. Wir ignorieren sie deshalb hier. if (dirty) then FBHealBox_RefreshAllBars(); end @@ -2643,6 +3836,16 @@ SlashCmdList["FBHEALPREDICT"] = function(msg) return; end + if (msg == "test") then + FBTest_Set(not FBTestMode); + return; + end + + if (msg == "config" or msg == "options" or msg == "opt") then + FBHealBox_ToggleOptions(); + 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); @@ -2687,6 +3890,12 @@ SlashCmdList["FBHEALPREDICT"] = function(msg) ..math.floor(s.max)); end + if (FBLOS_HasUnitXP()) then + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r LoS: UnitXP"); + else + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r LoS: UI_ERROR_MESSAGE ("..FBLOS_TIMEOUT.."s)"); + 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); @@ -2698,67 +3907,152 @@ SlashCmdList["FBHEALPREDICT"] = function(msg) end end end + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..FBT("FBP_COMMANDS")); +end + +-- Dispel-Farben je Klasse (einmal gebaut, nicht pro Update) +FBDispelColors = {}; +if (FBClass == "Priest") then + FBDispelColors["Magic"] = {0.2, 0.6, 1, 1}; + FBDispelColors["Disease"] = {0.6, 0.4, 0, 1}; +elseif (FBClass == "Paladin") then + FBDispelColors["Magic"] = {0.2, 0.6, 1, 1}; + FBDispelColors["Poison"] = {0, 0.6, 0, 1}; + FBDispelColors["Disease"] = {0.6, 0.4, 0, 1}; +elseif (FBClass == "Shaman") then + FBDispelColors["Poison"] = {0, 0.6, 0, 1}; + FBDispelColors["Disease"] = {0.6, 0.4, 0, 1}; +elseif (FBClass == "Druid") then + FBDispelColors["Curse"] = {0.6, 0, 1, 1}; + FBDispelColors["Poison"] = {0, 0.6, 0, 1}; +end + +-- Erster von der eigenen Klasse entfernbarer Debuff: Typ, Textur, Stacks (sonst nil) +function FBHealBox_DispelType(unit) + local g = FBTest_Ghost(unit); + if (g) then + -- Geist mit Debuff: irgendein Typ, den die eigene Klasse entfernen kann + if (g.debuff) then + for _, dtype in ipairs({ "Magic", "Poison", "Disease", "Curse" }) do + if (FBDispelColors[dtype]) then return dtype, g.debuffTex, g.debuffCount; end + end + end + return nil; + end + for i = 1, 16 do + local texture, count, debuffType = UnitDebuff(unit, i); + if (not texture) then break; end + if (debuffType and FBDispelColors[debuffType]) then return debuffType, texture, count; end + end + return nil; +end + +-- Debuff-Icon samt Stackzahl setzen oder verstecken +function FBHealBox_UpdateDebuffIcon(frame, texture, count) + if (not frame.DebuffIcon) then return; end + if (texture and HealBox.DebuffIcon == 1 and not frame.plateHidden) then + frame.NameText:SetWidth(frame.nameWidthIcon or FBNAME_WIDTH_ICON); + frame.DebuffIcon:SetTexture(texture); + frame.DebuffIcon:Show(); + if (count and tonumber(count) and tonumber(count) > 1) then + frame.DebuffCount:SetText(tostring(count)); + frame.DebuffCount:Show(); + else + frame.DebuffCount:Hide(); + end + else + frame.NameText:SetWidth(frame.nameWidthFull or FBNAME_WIDTH_FULL); + frame.DebuffIcon:Hide(); + frame.DebuffCount:Hide(); + end +end + +-- Manabalken: nur wenn eingeschaltet, die Plakette sichtbar ist und die +-- Einheit tatsaechlich Mana nutzt. Sonst weg, dann ist der Lebensbalken +-- wieder auf voller Hoehe zu sehen. +function FBHealBox_UpdateMana(unit, frame) + if (not frame.ManaBar) then return; end + if (HealBox.ManaBar ~= 1) or frame.plateHidden then + frame.ManaBar:Hide(); + return; + end + local mp, mpMax, hasMana = FBUnitMana(unit); + if (not hasMana) then + frame.ManaBar:Hide(); + return; + end + frame.ManaBar:SetMinMaxValues(0, mpMax); + frame.ManaBar:SetValue(mp); + frame.ManaBar:Show(); end function FBHealBox_UpdateUnit(unit, frame) - if not UnitExists(unit) then return end - local hp = UnitHealth(unit); - local hpMax = UnitHealthMax(unit); + if (not frame) or (not frame.HealthBar) then return; end + if (not FBUnitExists(unit)) then return; end + + local hp, hpMax = FBUnitHealth(unit); local hpPercent = hp / hpMax; - frame.HPText:SetText(format("%d%%", hpPercent * 100)); + local state = FBUnitState(unit); + + -- Tot / Geist / Offline: Text statt Prozent, Balken leer, grau, kein Mana + if (state) then + local label = "STATE_DEAD"; + if (state == "ghost") then label = "STATE_GHOST"; end + if (state == "offline") then label = "STATE_OFFLINE"; end + frame.HPText:SetText(FBT(label)); + frame.HealthBar:SetMinMaxValues(0, hpMax); + frame.HealthBar:SetValue(0); + frame.HealthBar:SetStatusBarColor(0.5, 0.5, 0.5, 1); + frame.ShieldBar:SetMinMaxValues(0, hpMax); + frame.ShieldBar:SetValue(0); + frame.IncHealBar:SetMinMaxValues(0, hpMax); + frame.IncHealBar:SetValue(0); + frame.ManaBar:Hide(); + FBHealBox_UpdateDebuffIcon(frame, nil, nil); + return; + end + + frame.HPText:SetText(format("%d%%", math.floor(hpPercent * 100))); frame.HealthBar:SetMinMaxValues(0, hpMax); frame.HealthBar:SetValue(hp); - local name = UnitName(unit); + local name = FBUnitName(unit); + local g = FBTest_Ghost(unit); -- Eigener Direktcast + eigene HoT-Restticks + Heilung anderer Heiler - local incHeal = FBGetDirectHeal(name) + FBGetHoTHeal(name) + FBGetCommHeal(name); + local incHeal, shield; + if (g) then + incHeal = g.inc or 0; + shield = g.shield or 0; + else + incHeal = FBGetDirectHeal(name) + FBGetHoTHeal(name) + FBGetCommHeal(name); + shield = FBGetShield(name); + end -- Absorb-Schild als Pseudoleben direkt hinter den aktuellen HP - local shieldTop = math.min(hpMax, hp + FBGetShield(name)); + local shieldTop = math.min(hpMax, hp + shield); 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)); + + -- Manabalken (Balken im Balken) + FBHealBox_UpdateMana(unit, frame); - 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 + -- Farbe: entfernbarer Debuff schlaegt den HP-Stand; dazu Icon + Stacks + local dtype, dtex, dcount = FBHealBox_DispelType(unit); + FBHealBox_UpdateDebuffIcon(frame, dtex, dcount); + if (dtype) then + local c = FBDispelColors[dtype]; + frame.HealthBar:SetStatusBarColor(c[1], c[2], c[3], c[4]); + elseif (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 diff --git a/FBHealBox.toc b/FBHealBox.toc index 0c1b496..ff32a6d 100644 --- a/FBHealBox.toc +++ b/FBHealBox.toc @@ -1,9 +1,9 @@ ## 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. +## Title: Heal Box Vanilla |cFFFFFF00v1.4.1|r +## Notes: Party, pet and self heal display with quick-cast buttons for healers. Ported to client 1.12.1 (optional SuperWoW). Up to 10 buttons per row, mana bar inside the health bar, class colours, buff watch, range fading, adjustable spacing, test mode with ghost players. Heal prediction for direct heals, HoT ticks and absorb shields, self-correcting from the combat log. HealComm sync with Puppeteer, pfUI and others. German/English. Original by Dourd, ported to Vanilla and extended 09/2026 by Mquadrat. +## Notes-deDE: Party-, Begleiter- und Selbst-Heilanzeige mit Schnellzugriff-Buttons fuer Heiler. Portiert fuer Client 1.12.1 (optional SuperWoW). Bis zu 10 Buttons pro Reihe, Manabalken im Lebensbalken, Klassenfarben, Buff-Wache, Reichweiten-Fading, einstellbare Abstaende, Testmodus mit Geisterspielern. Heal-Prediction fuer Direktheilung, HoT-Ticks und Absorb-Schilde, selbstkorrigierend ueber den Combatlog. HealComm-Sync mit Puppeteer, pfUI und Co. Deutsch/Englisch. 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 +## Version: 1.4.1 ## SavedVariablesPerCharacter: HealBox ## OptionalDeps: SuperWoW