diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d91ca..30ce83e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,47 @@ # Changelog -## 1.4.3 (unreleased) +## 1.4.4 (unreleased) + +Performance pass. No feature was added or changed; every visible behaviour is meant to be identical. A simulated five-second raid fight (40 players, aura and health events, mana changes, combat log, 60 frames per second) went from about 63,900 API and widget calls to about 14,450, a reduction of 77 %. + +### English + +**Changed (internals)** + +- **Button states centralised.** The up to 260 heal buttons (10 plates x 10, 40 raid cells x 4) no longer carry their own `SPELL_UPDATE_USABLE` / `SPELL_UPDATE_COOLDOWN` handlers. One pass over the *visible* buttons queries usability and cooldown once per spell id and range once per button, and sets colours and cooldown sweeps only when the state changes. Several such events within one frame are merged into one pass. +- **Display caches.** Health text, bar maxima, bar values, bar colour and mana strip visibility are only written when they change. Dispellable debuffs are searched on `UNIT_AURA` only; `UNIT_HEALTH` and the prediction tick reuse the last result. +- **Aura scans shared.** A unit's buff textures are read once per frame and shared between the heal prediction and the buff watch. Buff names resolved through tooltip scans are remembered per texture, so a unit missing the watched buff no longer costs up to 32 tooltip scans per aura event. Raid units are only scanned when something depends on them (own cast pending, tracked HoT or shield, or a visible cell). +- **Cheaper ticks.** Spell timers use precomputed base spell names instead of string parsing per button per tick and skip units without tracked HoTs or shields. Buff icons reuse their work tables, iterate a presorted list instead of sorting per tick, and update on change or once a second instead of five times a second. The attacked-member check skips entirely while there is no hostile target and nothing is marked, and compares names before calling `UnitIsUnit`. +- **Event bursts coalesced.** `PARTY_MEMBERS_CHANGED`, `UNIT_PET` and `RAID_ROSTER_UPDATE` set a flag that is processed once in the next frame instead of rebuilding plates and grid for every event of a burst. +- **Mana ticker.** Enabled/mana/full checks moved from every frame to mana events and option changes; the spark is re-anchored only when it has moved at least half a pixel; sizes and visibility are cached. +- **Combat log.** Heal patterns run only for buff events, the absorb pattern only for hit events that contain the word absorbed. Smart Damage parses damage lines only while enabled and while there is a target being measured or an own cast waiting; its live target line is refreshed only while the options window is open. +- Raid test ghost animation frame is hidden outside the raid test instead of running an empty update every frame. +- **Client API compatibility.** `IsSpellInRange` and `IsUsableSpell` do not exist in the 1.12 client (they came with 2.0). All range and usability checks now go through wrappers that use them when a client offers them and otherwise fall back to 1.12 means: spell range from the tooltip against `UnitXP("distanceBetween")` where available, else `CheckInteractDistance` (28 m, beyond that undecided rather than red); mana cost from the tooltip against your current mana. Because clients differ (2.0 style `(id, "spell", unit)` versus the Turtle client's `(name, unit)`), the addon probes both signatures once at login with `pcall` and uses the one the client understands; a function that keeps throwing is switched off in favour of the fallback. `/fbp` reports the detected form. Without this the new central button pass crashed at login. + +**Fixed** + +- The old per-button event handler declared `this`, `event` and `arg1` as parameters, which shadowed the 1.12 globals; range colouring and cooldown sweeps on the buttons never worked in the game. They do now. + +### Deutsch + +**Geaendert (intern)** + +- **Button-Zustaende zentral.** Die bis zu 260 Heil-Buttons (10 Plaketten x 10, 40 Raid-Zellen x 4) tragen keine eigenen `SPELL_UPDATE_USABLE`/`SPELL_UPDATE_COOLDOWN`-Handler mehr. Ein Durchgang ueber die *sichtbaren* Buttons fragt Nutzbarkeit und Cooldown je Zauber-ID einmal und die Reichweite je Button ab und setzt Farben und Cooldown-Uhren nur bei Zustandswechsel. Mehrere solche Events in einem Frame werden zu einem Durchgang zusammengefasst. +- **Anzeige-Zwischenspeicher.** Lebenstext, Balkenmaxima, Balkenwerte, Balkenfarbe und Sichtbarkeit des Manastreifens werden nur geschrieben, wenn sie sich aendern. Entfernbare Debuffs werden nur bei `UNIT_AURA` gesucht; `UNIT_HEALTH` und der Vorhersage-Tick nutzen den letzten Befund. +- **Aura-Scans gemeinsam.** Die Buff-Texturen einer Einheit werden je Frame einmal gelesen und von Heilvorhersage und Buff-Wache geteilt. Per Tooltip ermittelte Buffnamen werden je Textur gemerkt, sodass eine Einheit ohne den ueberwachten Buff nicht mehr bis zu 32 Tooltip-Scans je Aura-Event kostet. Raid-Einheiten werden nur gescannt, wenn etwas davon abhaengt (eigener Cast wartet, HoT oder Schild verfolgt, sichtbare Zelle). +- **Guenstigere Ticks.** Zauber-Timer nutzen vorberechnete Basisnamen statt String-Parsing je Button und Tick und ueberspringen Einheiten ohne verfolgten HoT oder Schild. Buff-Icons verwenden ihre Arbeitstabellen wieder, laufen ueber eine vorsortierte Liste statt je Tick zu sortieren und aktualisieren bei Aenderung oder einmal je Sekunde statt fuenfmal. Der Angegriffenen-Abgleich entfaellt ganz, solange kein feindliches Ziel besteht und nichts markiert ist, und vergleicht Namen, bevor `UnitIsUnit` gerufen wird. +- **Event-Salven zusammengefasst.** `PARTY_MEMBERS_CHANGED`, `UNIT_PET` und `RAID_ROSTER_UPDATE` setzen ein Kennzeichen, das im naechsten Frame einmal abgearbeitet wird, statt Plaketten und Raster bei jedem Event einer Salve neu aufzubauen. +- **Mana-Ticker.** Die Pruefungen an/Mana/voll laufen bei Mana-Events und Optionswechseln statt jeden Frame; der Funke wird nur neu verankert, wenn er sich mindestens einen halben Pixel bewegt hat; Groessen und Sichtbarkeit sind zwischengespeichert. +- **Combatlog.** Heilmuster laufen nur bei Buff-Events, das Absorb-Muster nur bei Treffer-Events, die das Wort absorbed enthalten. Smart Damage liest Schadenszeilen nur, solange es an ist und ein Ziel vermessen wird oder ein eigener Cast wartet; die Live-Zielzeile wird nur bei offenem Optionsfenster aktualisiert. +- Der Animationsframe der Raid-Testgeister ist ausserhalb des Raid-Tests ausgeblendet, statt jeden Frame leer zu laufen. +- **Client-API-Kompatibilitaet.** `IsSpellInRange` und `IsUsableSpell` gibt es im 1.12-Client nicht (sie kamen mit 2.0). Alle Reichweiten- und Nutzbarkeitspruefungen laufen jetzt ueber Wrapper, die sie nutzen, wenn ein Client sie anbietet, und sonst auf 1.12-Mittel zurueckfallen: Zauberreichweite aus dem Tooltip gegen `UnitXP("distanceBetween")`, wo vorhanden, sonst `CheckInteractDistance` (28 m, jenseits davon unentschieden statt rot); Manapreis aus dem Tooltip gegen das eigene Mana. Weil Clients sich unterscheiden (2.0-Form `(id, "spell", unit)` gegenueber der Turtle-Form `(name, unit)`), probiert das Addon beide Signaturen beim Login einmal per `pcall` aus und nutzt die verstandene; eine Funktion, die weiter Fehler wirft, wird zugunsten des Rueckfalls abgeschaltet. `/fbp` meldet die erkannte Form. Ohne das brach der neue zentrale Button-Durchgang beim Login ab. + +**Behoben** + +- Der alte Button-Handler deklarierte `this`, `event` und `arg1` als Parameter und verdeckte damit die 1.12-Globals; Reichweitenfaerbung und Cooldown-Uhr auf den Buttons haben im Spiel nie gearbeitet. Jetzt tun sie es. + + +## 1.4.3 (2026-09-05) ### English @@ -18,7 +59,7 @@ **Fixed** -- Own buffs were only read at login and on group changes: Vanilla fires `PLAYER_AURAS_CHANGED` for the player instead of `UNIT_AURA`. The addon now listens to it and re-reads your remaining time on every change, so recasting a buff on yourself resets its clock and cancelling it removes the icon. Also fixes confirmation of your own HoTs and shields on yourself. +- Own buffs were only read at login and on group changes: Vanilla fires `PLAYER_AURAS_CHANGED` for the player instead of `UNIT_AURA`. The addon now listens to it and re-reads your remaining time on every change, and once a second in between (a refresh of a running buff fires no aura event at all), so recasting a buff on yourself resets its clock and cancelling it removes the icon. Refreshing a buff on someone else through a HealBox button is confirmed by the completed cast. Also fixes confirmation of your own HoTs and shields on yourself. - Raid cell names and percentages were hidden behind the health bar since the bar levels became relative; the texts now live on their own layer above the bars. - Buff icons stack two high left of the plate; elapsed clock quadrants are drawn dark on a layer above the icon so the split shows on every client. Raid cells use a 3 by 4 grid of 6 px icons (twelve slots); the new raid option *Show buffs* removes icons and strip together. `/fbp buffs` reports the grey count per icon. - Test modes extended: party ghost Brynn is dead, pet Bramble carries a disease, Dorn carries six and raid ghost 12 twelve buffs, raid ghost 27 a disease. Up to six buff icons per plate. @@ -48,7 +89,7 @@ **Behoben** -- Eigene Buffs wurden nur beim Login und bei Gruppenwechseln gelesen: Vanilla feuert fuer den Spieler `PLAYER_AURAS_CHANGED` statt `UNIT_AURA`. Das Addon hoert jetzt darauf und liest die eigene Restzeit bei jeder Aenderung neu, sodass ein Neucast auf dich selbst die Uhr zuruecksetzt und ein Wegklicken das Icon entfernt. Behebt auch die Bestaetigung eigener HoTs und Schilde auf dir selbst. +- Eigene Buffs wurden nur beim Login und bei Gruppenwechseln gelesen: Vanilla feuert fuer den Spieler `PLAYER_AURAS_CHANGED` statt `UNIT_AURA`. Das Addon hoert jetzt darauf und liest die eigene Restzeit bei jeder Aenderung neu, und dazwischen einmal je Sekunde (ein Refresh eines laufenden Buffs feuert gar kein Aura-Event), sodass ein Neucast auf dich selbst die Uhr zuruecksetzt und ein Wegklicken das Icon entfernt. Ein Refresh auf jemand anderen ueber einen HealBox-Button gilt mit dem abgeschlossenen Cast als bestaetigt. Behebt auch die Bestaetigung eigener HoTs und Schilde auf dir selbst. - Namen und Prozente der Raid-Zellen lagen seit den relativen Balken-Ebenen hinter dem Lebensbalken; die Texte liegen jetzt auf einer eigenen Ebene ueber den Balken. - Buff-Icons stapeln sich zu zweit links neben der Plakette; abgelaufene Uhr-Quadranten werden dunkel auf einer Ebene ueber dem Icon gezeichnet, damit die Teilung auf jedem Client sichtbar ist. Raid-Zellen nutzen ein 3-mal-4-Raster aus 6-px-Icons (zwoelf Plaetze); die neue Raid-Option *Buffs anzeigen* entfernt Icons und Streifen gemeinsam. `/fbp buffs` nennt je Icon die Zahl grauer Quadranten. - Testmodi erweitert: Partygeist Brynn ist tot, Pet Bramble traegt eine Krankheit, Dorn traegt sechs und Raid-Geist 12 zwoelf Buffs, Raid-Geist 27 eine Krankheit. Bis zu sechs Buff-Icons je Plakette. diff --git a/FBHealBox.lua b/FBHealBox.lua index 070994d..016e783 100644 --- a/FBHealBox.lua +++ b/FBHealBox.lua @@ -17,6 +17,10 @@ -- Anzeige, Debuff-Icon mit Stackzahl -- * v1.4.2: Hook-Schnittstelle fuer Module (FBHealBox_RegisterHook, -- FBHealBox_AddOptionsTab). Der Raidmodus lebt in FBHealBox_Raid.lua. +-- * v1.4.4: Leistungsdurchgang ohne Funktionsaenderung: zentrale Button- +-- Zustaende statt 260 Event-Handler, Anzeige-Zwischenspeicher, Aura-Scan +-- je Frame nur einmal, Event-Salven zusammengefasst, Ticker per Event +-- gesteuert, Combatlog-Parser nach Eventklasse. Details im CHANGELOG. -- * v1.4.3: Mana-Ticker-Modul (FBHealBox_Ticker.lua), Smart-Damage-Modul -- (FBHealBox_Damage.lua, Abrangen von Angriffszaubern), Reiterknoepfe -- schmaler, damit vier Reiter ins Optionsfenster passen, Smart Healing @@ -73,7 +77,7 @@ HealBox = { -- feuert ADDON_LOADED fuer uns. FBADDON_NAME = "Heal Box Vanilla"; FBADDON_FOLDER = "FBHealBox"; -HealBoxVersion = "|cFFFFFF00v1.4.3|r"; +HealBoxVersion = "|cFFFFFF00v1.4.4|r"; -- ========================================================================== -- [ Lokalisierung / Localization ] @@ -1090,6 +1094,7 @@ function FBHealBox_OnLoad() this:RegisterEvent("UNIT_HEALTH"); this:RegisterEvent("UNIT_MAXHEALTH"); this:RegisterEvent("SPELL_UPDATE_COOLDOWN"); + this:RegisterEvent("SPELL_UPDATE_USABLE"); this:RegisterEvent("VARIABLES_LOADED"); this:RegisterEvent("UNIT_AURA"); -- Begleiter kommen und gehen (Beschwoerung, Wegschicken, Tod) @@ -1216,6 +1221,8 @@ function FBLoadSpellData() -- Tooltips auswerten: welche dieser Zauber sind HoTs bzw. Absorb-Schilde? FBPredict_BuildWatch(); + FBSpellNameCache = {}; + FBHealBox_ProbeAPIs(); -- Doppelt gezaehlte Absorb-Lernwerte (Versionen vor 1.4.2) verwerfen FBPredict_SanitizeMemory(); -- Watch ist neu: vorhandene Buffs sofort einlesen @@ -1755,9 +1762,20 @@ function FBHealBox_OnEvent(event, arg1) FBUpdateNames(); end - -- Gruppe oder Begleiter geaendert -> Plaketten neu belegen und anordnen + -- Button-Farben und Cooldowns: ein zentraler Durchgang statt 260 Handler. + -- Mehrere dieser Events je Frame (jede Manaaenderung feuert USABLE) + -- werden im naechsten OnUpdate zu einem Durchgang zusammengefasst. + if (event == "SPELL_UPDATE_USABLE") then + FBBtnStatesDirty = "SPELL_UPDATE_USABLE"; + elseif (event == "SPELL_UPDATE_COOLDOWN") then + if (FBBtnStatesDirty ~= "SPELL_UPDATE_USABLE") then FBBtnStatesDirty = "SPELL_UPDATE_COOLDOWN"; end + end + + -- Gruppe oder Begleiter geaendert -> Plaketten neu belegen und anordnen. + -- Salven (mehrere Events je Frame beim Zonen) werden auf einen Durchlauf + -- im naechsten Frame zusammengefasst. if (event == "PARTY_MEMBERS_CHANGED" or event == "UNIT_PET") then - FBUpdateNames(); + FBNamesDirty = true; end if (event == "UNIT_NAME_UPDATE") then @@ -1774,7 +1792,7 @@ function FBHealBox_OnEvent(event, arg1) 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]); + FBHealBox_UpdateUnit(arg1, FBPartyFrame[p], (event == "UNIT_AURA")); if (event == "UNIT_AURA") then FBHealBox_CheckWatchBuff(arg1, FBPartyFrame[p]); end end end @@ -2315,9 +2333,9 @@ function FBHealBoxCreateButton(FBButtonName, FBParentFrame, xoffset, yoffset, te end end); - button:RegisterEvent("SPELL_UPDATE_USABLE"); - button:RegisterEvent("SPELL_UPDATE_COOLDOWN"); - button:SetScript("OnEvent", HealBoxButton_OnEvent); + -- Kein eigener Event-Handler je Button mehr: der Kern aktualisiert alle + -- sichtbaren Buttons zentral in FBHealBox_UpdateButtonStates (ein + -- IsUsableSpell/GetSpellCooldown je Zauber statt je Button). return button; end @@ -2386,7 +2404,13 @@ end function FBUpdateNames() if (not FBHealBox1) then return; end - for p = 1, FBSlotCount do FBPartyFrame[p]:Hide(); end + for p = 1, FBSlotCount do + local f = FBPartyFrame[p]; + f:Hide(); + -- Einheit kann gewechselt haben: Zwischenspeicher der Anzeige leeren + f.dispelKnown = nil; f.lastText = nil; f.lastMax = nil; f.colorKey = nil; f.manaShown = nil; + f.vHp = nil; f.vShield = nil; f.vInc = nil; f.vMp = nil; + end if (HealBox.Active == 1) then FBHealBox1:Show(); end FBHealBox1.NameText:SetText(strupper(FBUnitName("player") or "")); @@ -2408,6 +2432,7 @@ function FBUpdateNames() FBHealBox_CheckLOSAll(); FBHealBox_RefreshAllBars(); FBHealBox_RunHook("UpdateNames"); + FBHealBox_UpdateButtonStates("SPELL_UPDATE_USABLE"); end -- Name in Klassenfarbe (Spieler) bzw. Pet-Farbe (Begleiter) @@ -2470,6 +2495,33 @@ function FBHealBox_UpdatePlateActionLabels() end end +-- Buff-Texturen einer Einheit, je Frame nur einmal gelesen: Vorhersage-Scan +-- und Buff-Wache brauchen dieselbe Liste im selben Event. +FBBuffScanMemo = {}; -- [unit] = { t, tex = {}, list = {}, n } +function FBHealBox_UnitBuffs(unit) + local now = GetTime(); + local m = FBBuffScanMemo[unit]; + if (m and m.t == now) then return m.tex, m.list, m.n; end + if (not m) then m = { tex = {}, list = {}, n = 0 }; FBBuffScanMemo[unit] = m; end + for k in pairs(m.tex) do m.tex[k] = nil; end + local i = 1; + while (i <= 32) do + local tex = UnitBuff(unit, i); + if (not tex) then break; end + tex = strupper(tex); + m.tex[tex] = true; + m.list[i] = tex; + i = i + 1; + end + m.n = i - 1; + m.t = now; + return m.tex, m.list, m.n; +end + +-- Buffname je Textur, einmal per Tooltip gelesen und dann gemerkt: erspart +-- bis zu 32 Tooltip-Scans je UNIT_AURA bei Einheiten ohne den Buff. +FBBuffNameByTex = {}; + -- Name eines Buffs ueber den Scan-Tooltip lesen function FBHealBox_BuffName(unit, index) if (not FBPredictTip) or (not FBPredictTip.SetUnitBuff) then return nil; end @@ -2492,23 +2544,19 @@ function FBHealBox_HasWatchBuff(unit) 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); + local texSet, list, n = FBHealBox_UnitBuffs(unit); + if (wantTex and texSet[wantTex]) then return true; end + if (altTex and texSet[altTex]) then return true; end + -- Textur nicht dabei: Namen pruefen (faengt fremde Gruppenversionen). + -- Je Textur nur einmal per Tooltip, danach aus dem Speicher. + for i = 1, n do + local tex = list[i]; + local name = FBBuffNameByTex[tex]; + if (name == nil) then + name = FBHealBox_BuffName(unit, i) or false; + FBBuffNameByTex[tex] = name; + end if (name and (name == want or name == alt)) then return true; end - i = i + 1; end return false; end @@ -2554,12 +2602,31 @@ end function FBHealBox_UnitIsAggro(unit, tt) if (not tt) or (not unit) then return false; end - if (UnitIsUnit) then return (UnitIsUnit(tt, unit) == 1) or (UnitIsUnit(tt, unit) == true); end + if (UnitIsUnit) then + local r = UnitIsUnit(tt, unit); + return (r == 1) or (r == true); + end return (UnitName(tt) == UnitName(unit)); end +FBAggroAny = false; -- ist irgendwo ein roter Rahmen gesetzt? + +-- Name des Angegriffenen je Tick einmal lesen; UnitIsUnit nur noch zur +-- Bestaetigung bei Namensgleichheit (statt je Plakette und Zelle) +FBAggroName = nil; + +function FBHealBox_UnitIsAggroFast(unit, unitName, tt) + if (not tt) or (not unitName) then return false; end + if (FBAggroName ~= unitName) then return false; end + return FBHealBox_UnitIsAggro(unit, tt); +end + function FBHealBox_CheckAggroAll() local tt = FBHealBox_AggroUnit(); + -- Kein feindliches Ziel und nichts markiert: nichts zu tun (haeufigster Fall) + if (not tt) and (not FBAggroAny) and (not FBTestMode) then return; end + FBAggroName = tt and UnitName(tt); + local any = false; for p = 1, FBSlotCount do local f = FBPartyFrame[p]; if (f) then @@ -2568,15 +2635,18 @@ function FBHealBox_CheckAggroAll() local g = FBTest_Ghost(unit); if (HealBox.AggroMark == 1 and f:IsShown()) then if (g) then flag = (g.aggro == true); - elseif (tt and FBUnitExists(unit)) then flag = FBHealBox_UnitIsAggro(unit, tt); end + elseif (tt and FBUnitExists(unit)) then flag = FBHealBox_UnitIsAggroFast(unit, FBUnitName(unit), tt); end end if (f.underAttack ~= flag) then f.underAttack = flag; FBHealBox_ApplyBorder(f); end + if (flag) then any = true; end end end - FBHealBox_RunHook("Aggro", tt); + -- Module (Raid-Zellen) melden ueber den Rueckgabewert, ob sie noch markieren + if (FBHealBox_RunHook("Aggro", tt)) then any = true; end + FBAggroAny = any; end -- ========================================================================== @@ -2588,15 +2658,15 @@ end -- werden. Module: Hook "SpellTimers". -- ========================================================================== --- Text und Farbe fuer einen Button, oder nil -function FBHealBox_SpellTimerFor(castString, unitName, now, g, btnIndex) +-- Text und Farbe fuer einen Button, oder nil. base ist der Zaubername ohne +-- Rang (einmal in ButtonsChanged berechnet, nicht je Tick). +function FBHealBox_SpellTimerFor(base, unitName, now, g, btnIndex) if (g) then if (btnIndex == 1 and g.hotLeft) then return tostring(g.hotLeft), FBTIMER_COLOR_HOT; end if (btnIndex == 2 and g.shieldLeft) then return tostring(g.shieldLeft), FBTIMER_COLOR_SHIELD; end return nil; end - if (not castString) or (not unitName) then return nil; end - local base = FBPredict_SplitCast(castString); + if (not base) or (not unitName) then return nil; end local hots = FBHoTs[unitName]; local e = hots and hots[base]; if (e and e.expires > now) then @@ -2624,10 +2694,11 @@ end function FBHealBox_SetButtonTimer(b, text, color) if (not b) or (not b.timer) then return; end if (text) then - b.timer:SetText(text); - b.timer:SetTextColor(color[1], color[2], color[3], 1); - b.timer:Show(); - else + if (b.timerText ~= text) then b.timerText = text; b.timer:SetText(text); end + if (b.timerColor ~= color) then b.timerColor = color; b.timer:SetTextColor(color[1], color[2], color[3], 1); end + if (not b.timerOn) then b.timerOn = true; b.timer:Show(); end + elseif (b.timerOn) then + b.timerOn = false; b.timerText = nil; b.timer:Hide(); end end @@ -2642,19 +2713,28 @@ function FBHealBox_UpdateSpellTimers() local shown = on and f:IsShown() and FBUnitExists(unit); local name = shown and FBUnitName(unit); local g = shown and FBTest_Ghost(unit); - for i = 1, MaxButtonCount do - local b = FBPartyTable[p][i]; - if (b) then - if (shown and b:IsShown()) then - local text, color = FBHealBox_SpellTimerFor(b.spellName, name, now, g, i); - if (not text and b.spellNameR) then - text, color = FBHealBox_SpellTimerFor(b.spellNameR, name, now, nil, i); + -- Ohne eigenen HoT oder Schild auf dieser Einheit gibt es nichts + -- anzuzeigen: dann nur einmal alle Timer ausblenden und weiter. + local active = shown and (g or FBHoTs[name] or FBShields[name]); + if (active) then + f.timersShown = true; + for i = 1, MaxButtonCount do + local b = FBPartyTable[p][i]; + if (b) then + if (b:IsShown()) then + local text, color = FBHealBox_SpellTimerFor(b.spellBase, name, now, g, i); + if (not text and b.spellBaseR) then + text, color = FBHealBox_SpellTimerFor(b.spellBaseR, name, now, nil, i); + end + FBHealBox_SetButtonTimer(b, text, color); + else + FBHealBox_SetButtonTimer(b, nil); end - FBHealBox_SetButtonTimer(b, text, color); - else - FBHealBox_SetButtonTimer(b, nil); end end + elseif (f.timersShown) then + f.timersShown = false; + for i = 1, MaxButtonCount do FBHealBox_SetButtonTimer(FBPartyTable[p][i], nil); end end end end @@ -2665,25 +2745,25 @@ end -- [ Cooldown-Uhr ] -- ========================================================================== +-- Cooldown eines Buttons frisch setzen (nach Umbelegung) function FBHealBox_UpdateButtonCooldown(b) if (not b) or (not b.cooldown) or (not CooldownFrame_SetTimer) then return; end - if (HealBox.Cooldowns ~= 1) or (not b.id) then + b.cdKey = nil; + if (not b.id) then CooldownFrame_SetTimer(b.cooldown, 0, 0, 0); + b.cdKey = "off"; return; end - local start, duration, enable = GetSpellCooldown(b.id, BOOKTYPE_SPELL); - if (start and duration and duration > FBCD_MIN_DURATION) then - CooldownFrame_SetTimer(b.cooldown, start, duration, enable); - else - CooldownFrame_SetTimer(b.cooldown, 0, 0, 0); - end + FBBtnPass = FBBtnPass + 1; + FBHealBox_UpdateButtonState(b, "SPELL_UPDATE_COOLDOWN"); end function FBHealBox_UpdateAllCooldowns() + FBBtnPass = FBBtnPass + 1; for p = 1, FBSlotCount do for i = 1, MaxButtonCount do local b = FBPartyTable[p] and FBPartyTable[p][i]; - if (b) then FBHealBox_UpdateButtonCooldown(b); end + if (b) then b.cdKey = nil; FBHealBox_UpdateButtonState(b, "SPELL_UPDATE_COOLDOWN"); end end end FBHealBox_RunHook("Cooldowns"); @@ -2771,6 +2851,145 @@ function FBHealBox_CheckLOSAll() end end +-- ========================================================================== +-- [ API-Fähigkeiten des Clients ] +-- +-- IsSpellInRange und IsUsableSpell gibt es erst seit TBC (2.0). Der +-- 1.12-Client hat sie nicht; ein Aufruf bricht mit "attempt to call global" +-- ab. Diese Wrapper nutzen sie, wenn vorhanden (SuperWoW-artige Clients), +-- und fallen sonst auf 1.12-Mittel zurueck: +-- Reichweite: UnitXP("distanceBetween") gegen die Tooltip-Reichweite +-- ("40 yd range"), sonst CheckInteractDistance (28 m). Was +-- jenseits von 28 m nicht entscheidbar ist, gilt als unbekannt +-- (kein Rot), nicht als ausser Reichweite. +-- Nutzbarkeit: Manapreis aus dem Tooltip ("175 Mana") gegen das eigene Mana. +-- ========================================================================== + +-- Signaturen unterscheiden sich je Client: 2.0 kennt (id, "spell", unit), +-- der Turtle-Client (name_oder_id, unit). Beim Einstieg wird mit pcall +-- ausprobiert, welche Form der Client versteht; keine -> 1.12-Rueckfall. +FBAPI_SpellRange = false; -- wird in FBHealBox_ProbeAPIs gesetzt +FBAPI_RangeForm = nil; -- 3 = (id, "spell", unit), 2 = (name, unit) +FBAPI_UsableSpell = false; +FBAPI_UsableForm = nil; -- 2 = (id, "spell"), 1 = (name) +FBAPI_Probed = false; +FBSpellRangeCache = {}; -- [bookID] = Reichweite in Metern oder false +FBSpellCostCache = {}; -- [bookID] = Manapreis oder false +FBSpellNameCache = {}; -- [bookID] = Zaubername (fuer die Namensformen) + +function FBHealBox_SpellNameOf(id) + local n = FBSpellNameCache[id]; + if (n) then return n; end + n = GetSpellName(id, BOOKTYPE_SPELL) or ""; + FBSpellNameCache[id] = n; + return n; +end + +-- Einmalig pruefen, welche Reichweiten-/Nutzbarkeitsfunktionen der Client +-- in welcher Form anbietet. Braucht ein gefuelltes Zauberbuch (Index 1). +function FBHealBox_ProbeAPIs() + FBAPI_Probed = true; + FBAPI_SpellRange = false; FBAPI_RangeForm = nil; + FBAPI_UsableSpell = false; FBAPI_UsableForm = nil; + if (type(IsSpellInRange) == "function") then + local ok = pcall(IsSpellInRange, 1, BOOKTYPE_SPELL, "player"); + if (ok) then + FBAPI_SpellRange = true; FBAPI_RangeForm = 3; + else + ok = pcall(IsSpellInRange, FBHealBox_SpellNameOf(1), "player"); + if (ok) then FBAPI_SpellRange = true; FBAPI_RangeForm = 2; end + end + end + if (type(IsUsableSpell) == "function") then + local ok = pcall(IsUsableSpell, 1, BOOKTYPE_SPELL); + if (ok) then + FBAPI_UsableSpell = true; FBAPI_UsableForm = 2; + else + ok = pcall(IsUsableSpell, FBHealBox_SpellNameOf(1)); + if (ok) then FBAPI_UsableSpell = true; FBAPI_UsableForm = 1; end + end + end +end + +-- Reichweite eines Zaubers aus dem Tooltip (Meter) oder nil +function FBHealBox_SpellRangeYards(id) + local c = FBSpellRangeCache[id]; + if (c ~= nil) then return c or nil; end + local txt = FBPredict_TooltipText and FBPredict_TooltipText(id) or ""; + local _, _, yd = string.find(txt, "(%d+)%s+[Yy]d"); + if (not yd) then _, _, yd = string.find(txt, "(%d+)%s+[Mm]eter"); end + c = yd and tonumber(yd) or false; + FBSpellRangeCache[id] = c; + return c or nil; +end + +-- Manapreis eines Zaubers aus dem Tooltip oder nil (Prozentkosten: nil) +function FBHealBox_SpellManaCost(id) + local c = FBSpellCostCache[id]; + if (c ~= nil) then return c or nil; end + local txt = FBPredict_TooltipText and FBPredict_TooltipText(id) or ""; + local _, _, cost = string.find(txt, "(%d+)%s+Mana"); + c = cost and tonumber(cost) or false; + FBSpellCostCache[id] = c; + return c or nil; +end + +-- 1 = in Reichweite, 0 = ausserhalb, nil = nicht entscheidbar +function FBHealBox_SpellInRange(id, unit) + if (not id) or (not unit) then return nil; end + if (not FBAPI_Probed) then FBHealBox_ProbeAPIs(); end + if (FBAPI_SpellRange) then + local ok, r; + if (FBAPI_RangeForm == 3) then + ok, r = pcall(IsSpellInRange, id, BOOKTYPE_SPELL, unit); + else + ok, r = pcall(IsSpellInRange, FBHealBox_SpellNameOf(id), unit); + end + if (ok) then + if (r == 1 or r == true) then return 1; end + if (r == 0 or r == false) then return 0; end + return nil; + end + -- Client wirft doch: Funktion abschalten, Rueckfall nutzen + FBAPI_SpellRange = false; + end + local range = FBHealBox_SpellRangeYards(id); + if (UnitXP and range) then + local ok, d = pcall(UnitXP, "distanceBetween", "player", unit); + if (ok and type(d) == "number") then + if (d <= range) then return 1; else return 0; end + end + end + if (CheckInteractDistance) then + if (CheckInteractDistance(unit, 4)) then return 1; end -- naeher als 28 m + if (range and range <= 28) then return 0; end -- Nahzauber: sicher ausserhalb + return nil; -- 28 bis 40 m: unbekannt + end + return nil; +end + +-- isUsable, noMana (wie IsUsableSpell) +function FBHealBox_SpellUsable(id) + if (not id) then return 1, nil; end + if (not FBAPI_Probed) then FBHealBox_ProbeAPIs(); end + if (FBAPI_UsableSpell) then + local ok, u, nm; + if (FBAPI_UsableForm == 2) then + ok, u, nm = pcall(IsUsableSpell, id, BOOKTYPE_SPELL); + else + ok, u, nm = pcall(IsUsableSpell, FBHealBox_SpellNameOf(id)); + end + if (ok) then return u, nm; end + FBAPI_UsableSpell = false; + end + local cost = FBHealBox_SpellManaCost(id); + if (cost) then + local mp = UnitMana("player") or 0; + if (UnitPowerType and UnitPowerType("player") == 0 and mp < cost) then return nil, 1; end + end + return 1, nil; +end + -- ========================================================================== -- [ Reichweiten-Fading ] -- @@ -2796,7 +3015,7 @@ function FBHealBox_UnitInRange(unit) if (not UnitExists(unit)) then return true; end local id = FBHealBox_RangeSpellID(); if (id) then - local r = IsSpellInRange(id, BOOKTYPE_SPELL, unit); + local r = FBHealBox_SpellInRange(id, unit); if (r == 0) then return false; end if (r == 1) then return true; end end @@ -3345,6 +3564,7 @@ function FBHealBoxCreateAddonOptionFrame() DebuffIconCheck = FBHealBox_CreateCheck("FBHealBoxDebuffIconCheck", tabGeneral, 40, cy - 90, "DEBUFFICON", "DEBUFFICON_TIP", function() HealBox.DebuffIcon = DebuffIconCheck:GetChecked() and 1 or 0; + FBHealBox_InvalidateUnitCaches(); FBHealBox_RefreshAllBars(); end); DebuffIconCheck:SetChecked(1); @@ -3568,11 +3788,16 @@ function FBHealBoxButtonsChanged() end end if (i <= (HealBox.MaxButtons or 0)) then b:Show(); end - FBHealBox_UpdateButtonCooldown(b); + b.cdKey = nil; + b.colorState = nil; + -- Basisnamen fuer die Timer einmal berechnen statt je Tick + if (b.spellName) then b.spellBase = FBPredict_SplitCast(b.spellName); else b.spellBase = nil; end + if (b.spellNameR) then b.spellBaseR = FBPredict_SplitCast(b.spellNameR); else b.spellBaseR = nil; end end end end FBHealBox_RunHook("ButtonsChanged"); + FBHealBox_UpdateButtonStates("SPELL_UPDATE_USABLE"); end -- Buttons neu verketten: erster Button haengt an der Plakette, jeder weitere @@ -3708,28 +3933,92 @@ function FBGetSpellID(spell, rank, debug) return spellID, highestRank; end -function HealBoxButton_OnEvent(this, event, arg1) - if (HealBox.Active == 0) then return 0; end - if (event == "SPELL_UPDATE_COOLDOWN" or event == "SPELL_UPDATE_USABLE") then - FBHealBox_UpdateButtonCooldown(this); - end - if event=="SPELL_UPDATE_USABLE" then - if (this.id) then - local isUsable, noMana = IsUsableSpell(this.id, BOOKTYPE_SPELL); - icon = getglobal(this:GetName()); - if isUsable then - this.icon:SetVertexColor(1.0, 1.0, 1.0); - elseif noMana then - this.icon:SetVertexColor(0.5, 0.5, 1.0); - else - this.icon:SetVertexColor(0.3, 0.3, 0.3); - end - local inRange = IsSpellInRange(this.id, BOOKTYPE_SPELL, this.TargetUnit); - if (inRange == 0) then - this.icon:SetVertexColor(1.0, 0.3, 0.3); - end - end - end +-- ========================================================================== +-- [ Button-Zustaende zentral ] +-- +-- Frueher hatte jeder der bis zu 260 Buttons einen eigenen Event-Handler +-- fuer SPELL_UPDATE_USABLE/COOLDOWN (der zudem "this"/"event" als Parameter +-- deklarierte und damit die 1.12-Globals verdeckte, also nie etwas tat). +-- Jetzt laeuft ein Durchgang ueber die sichtbaren Buttons; Nutzbarkeit und +-- Cooldown werden je Zauber-ID nur einmal abgefragt, die Reichweite je +-- Button (haengt vom Ziel ab). Farben werden nur gesetzt, wenn sich der +-- Zustand aendert. +-- ========================================================================== + +FBBtnUsableCache = {}; -- [id] = { pass, st } st = "ok" | "mana" | "no" +FBBtnCdCache = {}; -- [id] = { pass, start, duration, enable } +FBBtnPass = 0; -- Durchgangszaehler: Eintraege eines aelteren Durchgangs gelten als leer + +function FBHealBox_ButtonUsable(id) + local e = FBBtnUsableCache[id]; + if (e and e.pass == FBBtnPass) then return e.st; end + if (not e) then e = {}; FBBtnUsableCache[id] = e; end + local isUsable, noMana = FBHealBox_SpellUsable(id); + if (isUsable) then e.st = "ok"; elseif (noMana) then e.st = "mana"; else e.st = "no"; end + e.pass = FBBtnPass; + return e.st; +end + +function FBHealBox_ButtonCooldown(id) + local cd = FBBtnCdCache[id]; + if (cd and cd.pass == FBBtnPass) then return cd; end + if (not cd) then cd = {}; FBBtnCdCache[id] = cd; end + local start, duration, enable = GetSpellCooldown(id, BOOKTYPE_SPELL); + cd[1] = start or 0; cd[2] = duration or 0; cd[3] = enable or 0; + cd.pass = FBBtnPass; + return cd; +end + +-- Einen sichtbaren Button nachfuehren. Nutzbarkeit/Reichweite nur bei USABLE. +function FBHealBox_UpdateButtonState(b, event) + if (not b) or (not b.id) then return; end + if (b.cooldown and CooldownFrame_SetTimer) then + local key = "off"; + local cd = nil; + if (HealBox.Cooldowns == 1) then + cd = FBHealBox_ButtonCooldown(b.id); + if (cd[2] > FBCD_MIN_DURATION) then key = cd[1].."|"..cd[2]; end + end + if (b.cdKey ~= key) then + b.cdKey = key; + if (key == "off") then CooldownFrame_SetTimer(b.cooldown, 0, 0, 0); + else CooldownFrame_SetTimer(b.cooldown, cd[1], cd[2], cd[3]); end + end + end + if (event ~= "SPELL_UPDATE_USABLE") then return; end + local st = FBHealBox_ButtonUsable(b.id); + local inRange = FBHealBox_SpellInRange(b.id, b.TargetUnit); + if (inRange == 0) then st = "range"; end + if (b.colorState == st) then return; end + b.colorState = st; + if (st == "ok") then b.icon:SetVertexColor(1.0, 1.0, 1.0); + elseif (st == "mana") then b.icon:SetVertexColor(0.5, 0.5, 1.0); + elseif (st == "range") then b.icon:SetVertexColor(1.0, 0.3, 0.3); + else b.icon:SetVertexColor(0.3, 0.3, 0.3); end +end + +-- Alle sichtbaren Buttons (Plaketten; Module ueber Hook "ButtonStates") +function FBHealBox_UpdateButtonStates(event) + if (HealBox.Active == 0) then return; end + FBBtnPass = FBBtnPass + 1; + local maxB = HealBox.MaxButtons or 0; + for p = 1, FBSlotCount do + local f = FBPartyFrame[p]; + if (f and f:IsShown() and FBPartyTable[p]) then + for i = 1, maxB do + local b = FBPartyTable[p][i]; + if (b and b:IsShown()) then FBHealBox_UpdateButtonState(b, event); end + end + end + end + FBHealBox_RunHook("ButtonStates", event); +end + +-- Kompatibilitaet fuer aeltere Aufrufer +function HealBoxButton_OnEvent(this, event, arg1) + if (HealBox.Active == 0) then return 0; end + FBBtnPass = FBBtnPass + 1; + FBHealBox_UpdateButtonState(this, event); end function HealBoxScale(this, scale) @@ -4050,9 +4339,12 @@ function FBPredict_GetSpellInfo(bookID, spellName) end -- Nach jedem Zauberbuch-Scan: was koennen die gelernten Zauber ueberhaupt? +FBPredictWatchBuffOrder = {}; -- Buff-Zauber alphabetisch (fuer die Icon-Reihenfolge) + function FBPredict_BuildWatch() FBPredictWatch = {}; FBPredictInfo = {}; + FBPredictWatchBuffOrder = {}; for spellName, ranks in pairs(FBPlayerSpells) do local top = ranks[table.getn(ranks)]; @@ -4069,15 +4361,34 @@ function FBPredict_BuildWatch() buffSecs = info.buff and info.buff.duration, icon = top.icon, }; + if (info.buff) then table.insert(FBPredictWatchBuffOrder, spellName); end end end + table.sort(FBPredictWatchBuffOrder); +end + +-- Buff per eigenem Button auf ein Ziel neu gewirkt, das ihn schon traegt: +-- ein Refresh loest kein Aura-Event aus, also gilt der erfolgreiche Cast +-- (SPELLCAST_STOP) als Bestaetigung und stellt die Uhr auf voll. +function FBPredict_ConfirmBuffRefresh() + local p = FBPredictPending; + if (not p) or (not p.spell) or (not p.target) then return; end + local w = FBPredictWatch[p.spell]; + if (not w) or (not w.hasBuff) then return; end + if (p.target == UnitName("player")) then return; end -- eigene Buffs liest die Buff-API + if (FBBuffPresent[p.target] and FBBuffPresent[p.target][p.spell]) then + FBPredict_StartBuff(p.target, p.spell, w.buffSecs); + FBPredictPending = nil; + end end -- Buff-Laufzeit auf einer Einheit vermerken function FBPredict_StartBuff(unitName, spellName, secs) if (not unitName) or (not spellName) or (not secs) then return; end if (not FBBuffTimers[unitName]) then FBBuffTimers[unitName] = {}; end - FBBuffTimers[unitName][spellName] = { expires = GetTime() + secs }; + local t = FBBuffTimers[unitName][spellName]; + if (t) then t.expires = GetTime() + secs; else FBBuffTimers[unitName][spellName] = { expires = GetTime() + secs }; end + FBBuffIconsDirty = true; end -- Restlaufzeit eines eigenen Buffs (Textur) ueber die Spielerbuff-API @@ -4352,15 +4663,20 @@ function FBPredict_ScanUnit(unit) local name = UnitName(unit); if (not name) then return; end - local textures = {}; - local i = 1; - while (i <= 32) do - local tex = UnitBuff(unit, i); - if (not tex) then break; end - textures[strupper(tex)] = true; - i = i + 1; + -- Raid-Einheiten: nur scannen, wenn etwas davon abhaengt (eigener Cast + -- wartet, HoT/Schild wird verfolgt, oder die Zelle ist zu sehen). Sonst + -- kosten 40 Raider mit staendig wechselnden Auren nur CPU. + if (string.sub(unit, 1, 4) == "raid") then + local needed = (FBPredictPending and FBPredictPending.target == name) + or FBHoTs[name] or FBShields[name]; + if (not needed) then + local c = FBRaidUnitCell and FBRaidUnitCell[unit]; + if (not c) or (not c:IsShown()) then return; end + end end + local textures = FBHealBox_UnitBuffs(unit); + local dirty = false; -- 1) Eigener Cast wartet auf Bestaetigung? Der hat Vorrang, damit auch @@ -4390,6 +4706,7 @@ function FBPredict_ScanUnit(unit) for spellName, w in pairs(FBPredictWatch) do if (w.hasBuff) then local present = textures[w.tex]; + if (FBBuffPresent[name][spellName] ~= present) then FBBuffIconsDirty = true; end FBBuffPresent[name][spellName] = present; local tracked = FBBuffTimers[name] and FBBuffTimers[name][spellName]; if (present and unit == "player") then @@ -4569,9 +4886,33 @@ function FBPredict_OnAbsorb(unitName, amount) FBHealBox_RefreshAllBars(); end +-- Eventklasse je Eventname einmal bestimmen: Heil-/Tick-Zeilen kommen nur +-- ueber *_BUFF(S)-Events, Absorb-Zeilen nur ueber Treffer-Events. So laufen +-- je Zeile hoechstens die passenden Muster statt aller fuenf. +FBPredictEventClass = {}; +function FBPredict_EventClass(event) + local c = FBPredictEventClass[event]; + if (c) then return c; end + if (string.find(event, "_BUFF")) then c = "heal"; else c = "hit"; end + FBPredictEventClass[event] = c; + return c; +end + function FBPredict_ParseCombat(event, msg) if (not msg) then return; end + if (FBPredict_EventClass(event) == "hit") then + -- Absorb-Anteil: "... hits Bob for 120. (80 absorbed)" (billiger Vortest) + if (string.find(msg, "absorbed", 1, true)) then + local _, _, abs = string.find(msg, FBPredictPatAbsorb); + if (abs) then + local victim = FBPredict_ResolveVictim(event, msg); + if (victim) then FBPredict_OnAbsorb(victim, tonumber(abs)); end + end + end + return; + end + -- Direktheilung auf jemand anderen: "Your Flash Heal heals Bob for 1240." local _, _, spell, who, amt = string.find(msg, FBPredictPatHealOther); if (spell and who and amt) then @@ -4599,13 +4940,6 @@ function FBPredict_ParseCombat(event, msg) FBPredict_OnTick(UnitName("player"), tonumber(amt4), spell4); return; end - - -- Absorb-Anteil: "... hits Bob for 120. (80 absorbed)" - local _, _, abs = string.find(msg, FBPredictPatAbsorb); - if (abs) then - local victim = FBPredict_ResolveVictim(event, msg); - if (victim) then FBPredict_OnAbsorb(victim, tonumber(abs)); end - end end -- [ Event-Frame ] ---------------------------------------------------------- @@ -4667,6 +5001,7 @@ FBPredictFrame:SetScript("OnEvent", function() elseif (event == "SPELLCAST_STOP") then -- Erfolgreich beendet: HealComm-Empfaenger lassen den Eintrag -- selbst auslaufen, hier wird bewusst kein Healstop gefunkt. + FBPredict_ConfirmBuffRefresh(); FBPredict_CastEnd(); elseif (event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED") then @@ -4688,7 +5023,24 @@ FBPredictFrame:SetScript("OnUpdate", function() FBPredict_OnUpdate(arg1); end); +FBNamesDirty = false; +FBBtnStatesDirty = nil; +FBBuffIconsDirty = true; +FBBuffIconsAccum = 0; + function FBPredict_OnUpdate(elapsed) + -- Aufgeschobene Gruppen-Aktualisierung (Event-Salven zusammengefasst) + if (FBNamesDirty) then + FBNamesDirty = false; + FBUpdateNames(); + end + -- Aufgeschobene Button-Zustaende (USABLE schlaegt COOLDOWN, da es beides tut) + if (FBBtnStatesDirty) then + local ev = FBBtnStatesDirty; + FBBtnStatesDirty = nil; + FBHealBox_UpdateButtonStates(ev); + end + -- Reichweiten-Fading laeuft im eigenen Takt mit FBRangeAccum = FBRangeAccum + (elapsed or 0); if (FBRangeAccum >= FBRANGE_INTERVAL) then @@ -4701,10 +5053,18 @@ function FBPredict_OnUpdate(elapsed) if (FBPredictAccum < FBPREDICT_THROTTLE) then return; end FBPredictAccum = 0; - -- Angegriffenen markieren, Button-Timer und Buff-Icons nachfuehren (0.2-s-Takt) + -- Angegriffenen markieren, Button-Timer und Buff-Icons nachfuehren (0.2-s-Takt). + -- Die Uhr-Quadranten aendern sich langsam: Buff-Icons nur bei Aenderung + -- (Scan hat etwas gemeldet) oder einmal je Sekunde. FBHealBox_CheckAggroAll(); FBHealBox_UpdateSpellTimers(); - FBHealBox_UpdateAllBuffIcons(); + FBPredict_RefreshPlayerBuffs(FBPREDICT_THROTTLE); + FBBuffIconsAccum = FBBuffIconsAccum + FBPREDICT_THROTTLE; + if (FBBuffIconsDirty or FBBuffIconsAccum >= 1.0 or FBTestMode) then + FBBuffIconsDirty = false; + FBBuffIconsAccum = 0; + FBHealBox_UpdateAllBuffIcons(); + end local now = GetTime(); local dirty = false; @@ -5069,6 +5429,11 @@ SlashCmdList["FBHEALPREDICT"] = function(msg) else DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r LoS: UI_ERROR_MESSAGE ("..FBLOS_TIMEOUT.."s)"); end + local rangeApi = "CheckInteractDistance (28m)"; + if (FBAPI_SpellRange) then rangeApi = "IsSpellInRange ("..tostring(FBAPI_RangeForm).." args)"; elseif (UnitXP) then rangeApi = "UnitXP distance + tooltip"; end + local usableApi = "tooltip mana cost"; + if (FBAPI_UsableSpell) then usableApi = "IsUsableSpell ("..tostring(FBAPI_UsableForm).." args)"; end + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r Range: "..rangeApi..", usable: "..usableApi); local state = FBT("FBP_STATE_OFF"); if (FBComm_Enabled()) then state = FBT("FBP_STATE_ON"); end @@ -5105,6 +5470,8 @@ elseif (FBClass == "Druid") then FBDispelColors["Poison"] = {0, 0.6, 0, 1}; end +FBDISPEL_ORDER = { "Magic", "Poison", "Disease", "Curse" }; + -- Erster von der eigenen Klasse entfernbarer Debuff: Typ, Textur, Stacks (sonst nil) function FBHealBox_DispelType(unit) local g = FBTest_Ghost(unit); @@ -5116,7 +5483,7 @@ function FBHealBox_DispelType(unit) end -- Geist mit Debuff: irgendein Typ, den die eigene Klasse entfernen kann if (g.debuff) then - for _, dtype in ipairs({ "Magic", "Poison", "Disease", "Curse" }) do + for _, dtype in ipairs(FBDISPEL_ORDER) do if (FBDispelColors[dtype]) then return dtype, g.debuffTex, g.debuffCount; end end end @@ -5275,29 +5642,33 @@ function FBHealBox_SetBuffIconProgress(ic, remaining) end end --- Liste der zu zeigenden Buffs einer Einheit: { tex, expires, duration } -function FBHealBox_BuffIconList(unitName, g) - local list = {}; +-- Zu zeigende Buffs einer Einheit in die Arbeits-Tabelle out schreiben +-- (wiederverwendet, keine neuen Tabellen je Tick). Liefert die Anzahl. +-- Reihenfolge: FBPredictWatchBuffOrder (einmal in BuildWatch sortiert). +function FBHealBox_BuffIconList(unitName, g, out) + local n = 0; if (g) then for _, b in ipairs(g.buffs or {}) do - table.insert(list, { tex = b.tex, expires = b.left and (GetTime() + b.left), duration = b.dur, name = "Test" }); + n = n + 1; + local e = out[n] or {}; out[n] = e; + e.tex = b.tex; e.expires = b.left and (GetTime() + b.left); e.duration = b.dur; e.name = "Test"; end - return list; + return n; end - if (not unitName) then return list; end + if (not unitName) then return 0; end local present = FBBuffPresent[unitName]; - if (not present) then return list; end - local names = {}; - for spellName, w in pairs(FBPredictWatch) do - if (w.hasBuff and present[spellName]) then table.insert(names, spellName); end + if (not present) then return 0; end + local timers = FBBuffTimers[unitName]; + for _, spellName in ipairs(FBPredictWatchBuffOrder) do + if (present[spellName]) then + local w = FBPredictWatch[spellName]; + local bt = timers and timers[spellName]; + n = n + 1; + local e = out[n] or {}; out[n] = e; + e.tex = w.icon; e.expires = bt and bt.expires; e.duration = w.buffSecs; e.name = spellName; + end end - table.sort(names); - for _, spellName in ipairs(names) do - local w = FBPredictWatch[spellName]; - local bt = FBBuffTimers[unitName] and FBBuffTimers[unitName][spellName]; - table.insert(list, { tex = w.icon, expires = bt and bt.expires, duration = w.buffSecs, name = spellName }); - end - return list; + return n; end -- Icons einer Plakette (oder Raid-Zelle) nachfuehren. Liefert die Anzahl. @@ -5306,14 +5677,19 @@ end function FBHealBox_UpdateBuffIcons(frame, unitName, g, size, rows, maxIcons) if (not frame) or (not frame.HPText) then return 0; end maxIcons = maxIcons or FBBUFFICON_MAX; - local list = {}; + if (not frame.buffList) then frame.buffList = {}; end + local list = frame.buffList; + local count = 0; if (HealBox.BuffIcons == 1 and not frame.plateHidden) then - list = FBHealBox_BuffIconList(unitName, g); + count = FBHealBox_BuffIconList(unitName, g, list); end + -- nichts zu tun, wenn weder etwas angezeigt wird noch angezeigt war + if (count == 0 and (frame.buffIconCount or 0) == 0) then return 0; end local now = GetTime(); local n = 0; for k = 1, maxIcons do - local b = list[k]; + local b = nil; + if (k <= count) then b = list[k]; end local ic = (frame.buffIcons and frame.buffIcons[k]) or (b and FBHealBox_GetBuffIcon(frame, k, size, rows)); if (ic) then if (b and b.tex) then @@ -5348,6 +5724,33 @@ function FBHealBox_UpdateBuffIcons(frame, unitName, g, size, rows, maxIcons) return n; end +-- Eigene Buffs regelmaessig neu aus der Buff-API lesen: Ein Neucast auf +-- einen noch laufenden Buff (Refresh) loest im 1.12-Client kein Aura-Event +-- aus, die Restzeit springt aber trotzdem auf voll. +FBBuffRefreshAccum = 0; +function FBPredict_RefreshPlayerBuffs(elapsed) + FBBuffRefreshAccum = FBBuffRefreshAccum + (elapsed or 0); + if (FBBuffRefreshAccum < 1.0) then return; end + FBBuffRefreshAccum = 0; + local me = UnitName("player"); + if (not me) or (not FBBuffPresent[me]) then return; end + local timers = FBBuffTimers[me]; + for _, spellName in ipairs(FBPredictWatchBuffOrder) do + if (FBBuffPresent[me][spellName]) then + local w = FBPredictWatch[spellName]; + local left = FBPredict_PlayerBuffTimeLeft(w.tex); + if (left) then + -- nur uebernehmen, wenn die API mehr als eine Sekunde von der + -- eigenen Uhr abweicht (Refresh); sonst kein Dirty-Flag + local t = timers and timers[spellName]; + if (not t) or (math.abs((t.expires - GetTime()) - left) > 1.0) then + FBPredict_StartBuff(me, spellName, left); + end + end + end + end +end + function FBHealBox_UpdateAllBuffIcons() for p = 1, FBSlotCount do local f = FBPartyFrame[p]; @@ -5368,21 +5771,56 @@ end -- 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; + local mp, mpMax, hasMana = 0, 0, false; + if (HealBox.ManaBar == 1) and (not frame.plateHidden) then + mp, mpMax, hasMana = FBUnitMana(unit); end - local mp, mpMax, hasMana = FBUnitMana(unit); if (not hasMana) then - frame.ManaBar:Hide(); + if (frame.manaShown ~= false) then frame.manaShown = false; frame.ManaBar:Hide(); end return; end - frame.ManaBar:SetMinMaxValues(0, mpMax); - frame.ManaBar:SetValue(mp); - frame.ManaBar:Show(); + if (frame.lastMpMax ~= mpMax) then + frame.lastMpMax = mpMax; + frame.ManaBar:SetMinMaxValues(0, mpMax); + end + if (frame.vMp ~= mp) then frame.vMp = mp; frame.ManaBar:SetValue(mp); end + if (frame.manaShown ~= true) then frame.manaShown = true; frame.ManaBar:Show(); end end -function FBHealBox_UpdateUnit(unit, frame) +-- Anzeige-Zwischenspeicher aller Plaketten leeren (nach Optionswechsel) +function FBHealBox_InvalidateUnitCaches() + for p = 1, FBSlotCount do + local f = FBPartyFrame[p]; + if (f) then f.dispelKnown = nil; f.lastText = nil; f.lastMax = nil; f.colorKey = nil; f.manaShown = nil; f.lastMpMax = nil; f.vHp = nil; f.vShield = nil; f.vInc = nil; f.vMp = nil; end + end +end + +-- Balkenfarbe nur setzen, wenn sie sich aendert (Schluessel merken) +function FBHealBox_SetBarColor(frame, key, r, g, b, a) + if (frame.colorKey == key) then return; end + frame.colorKey = key; + frame.HealthBar:SetStatusBarColor(r, g, b, a); +end + +-- Werte der drei Balken nur setzen, wenn sie sich geaendert haben +function FBHealBox_SetBarValues(frame, hp, shieldTop, incTop) + if (frame.vHp ~= hp) then frame.vHp = hp; frame.HealthBar:SetValue(hp); end + if (frame.vShield ~= shieldTop) then frame.vShield = shieldTop; frame.ShieldBar:SetValue(shieldTop); end + if (frame.vInc ~= incTop) then frame.vInc = incTop; frame.IncHealBar:SetValue(incTop); end +end + +-- Maximalwerte der drei Balken nur bei geaendertem hpMax setzen +function FBHealBox_SetBarMax(frame, hpMax) + if (frame.lastMax == hpMax) then return; end + frame.lastMax = hpMax; + frame.HealthBar:SetMinMaxValues(0, hpMax); + frame.ShieldBar:SetMinMaxValues(0, hpMax); + frame.IncHealBar:SetMinMaxValues(0, hpMax); +end + +-- auraChanged: true bei UNIT_AURA. Nur dann wird der Debuff neu gesucht +-- (UnitDebuff-Scan); UNIT_HEALTH und der Vorhersage-Tick nutzen den Cache. +function FBHealBox_UpdateUnit(unit, frame, auraChanged) if (not frame) or (not frame.HealthBar) then return; end if (not FBUnitExists(unit)) then return; end @@ -5395,22 +5833,24 @@ function FBHealBox_UpdateUnit(unit, frame) 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(); + if (frame.lastText ~= label) then + frame.lastText = label; + frame.HPText:SetText(FBT(label)); + end + FBHealBox_SetBarMax(frame, hpMax); + FBHealBox_SetBarValues(frame, 0, 0, 0); + FBHealBox_SetBarColor(frame, "state", 0.5, 0.5, 0.5, 1); + if (frame.manaShown ~= false) then frame.manaShown = false; frame.ManaBar:Hide(); end 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 pct = math.floor(hpPercent * 100); + if (frame.lastText ~= pct) then + frame.lastText = pct; + frame.HPText:SetText(pct.."%"); + end + FBHealBox_SetBarMax(frame, hpMax); local name = FBUnitName(unit); local g = FBTest_Ghost(unit); @@ -5425,30 +5865,34 @@ function FBHealBox_UpdateUnit(unit, frame) shield = FBGetShield(name); end - -- Absorb-Schild als Pseudoleben direkt hinter den aktuellen HP - 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)); + -- Absorb-Schild als Pseudoleben direkt hinter den aktuellen HP, + -- Heilvorhersage dahinter; Werte nur setzen, wenn sie sich aendern + local shieldTop = hp + shield; + if (shieldTop > hpMax) then shieldTop = hpMax; end + local incTop = shieldTop + incHeal; + if (incTop > hpMax) then incTop = hpMax; end + FBHealBox_SetBarValues(frame, hp, shieldTop, incTop); -- Manabalken (Balken im Balken) FBHealBox_UpdateMana(unit, frame); - -- Farbe: entfernbarer Debuff schlaegt den HP-Stand; dazu Icon + Stacks - local dtype, dtex, dcount = FBHealBox_DispelType(unit); - FBHealBox_UpdateDebuffIcon(frame, dtex, dcount); + -- Farbe: entfernbarer Debuff schlaegt den HP-Stand; dazu Icon + Stacks. + -- Debuffs aendern sich nur mit UNIT_AURA, sonst gilt der letzte Befund. + if (auraChanged or (not frame.dispelKnown) or g) then + frame.dispelType, frame.dispelTex, frame.dispelCount = FBHealBox_DispelType(unit); + frame.dispelKnown = true; + FBHealBox_UpdateDebuffIcon(frame, frame.dispelTex, frame.dispelCount); + end + local dtype = frame.dispelType; if (dtype) then local c = FBDispelColors[dtype]; - frame.HealthBar:SetStatusBarColor(c[1], c[2], c[3], c[4]); + FBHealBox_SetBarColor(frame, dtype, c[1], c[2], c[3], c[4]); elseif (hpPercent > LowHP) then - frame.HealthBar:SetStatusBarColor(0, 1, 0, 1); + FBHealBox_SetBarColor(frame, "green", 0, 1, 0, 1); elseif (hpPercent > VeryLowHP) then - frame.HealthBar:SetStatusBarColor(1, 0.9, 0, 1); + FBHealBox_SetBarColor(frame, "yellow", 1, 0.9, 0, 1); else - frame.HealthBar:SetStatusBarColor(1, 0, 0, 1); + FBHealBox_SetBarColor(frame, "red", 1, 0, 0, 1); end end diff --git a/FBHealBox.toc b/FBHealBox.toc index d7302ae..8dcfd08 100644 --- a/FBHealBox.toc +++ b/FBHealBox.toc @@ -1,9 +1,9 @@ ## Interface: 11200 -## Title: Heal Box Vanilla |cFFFFFF00v1.4.3|r +## Title: Heal Box Vanilla |cFFFFFF00v1.4.4|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, optional raid mode for 20 and 40 players, mana ticker with five-second rule, smart damage ranking for attack spells. 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, optionaler Raidmodus fuer 20 und 40 Spieler, Mana-Ticker mit Fuenf-Sekunden-Regel, Smart Damage fuer Angriffszauber. 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.3 +## Version: 1.4.4 ## SavedVariablesPerCharacter: HealBox ## OptionalDeps: SuperWoW diff --git a/FBHealBox_Damage.lua b/FBHealBox_Damage.lua index d3c2581..5fe995e 100644 --- a/FBHealBox_Damage.lua +++ b/FBHealBox_Damage.lua @@ -338,6 +338,10 @@ function FBDmg_ParseLine(msg) end function FBDmg_OnCombatLog(msg) + -- Nur arbeiten, wenn Smart Damage an ist und es etwas zu lernen gibt + -- (Prozentziel wird vermessen oder ein eigener Cast wartet auf Treffer). + if (FBDmg_Cfg().Enabled ~= 1) then return; end + if (not FBDmgTargetName) and (not FBDmgLastCast) then return; end local victim, amount, spell, who = FBDmg_ParseLine(msg); if (not victim) then return; end FBDmg_NoteDamage(victim, amount); @@ -494,7 +498,10 @@ function FBDmg_SourceText() end function FBDmg_UpdateSourceText() - if (FBDmgSourceText) then FBDmgSourceText:SetText(format(FBT("DMG_SRC"), FBDmg_SourceText())); end + -- nur, wenn das Optionsfenster offen ist (sonst je UNIT_HEALTH des Ziels umsonst) + if (FBDmgSourceText and panel and panel:IsVisible()) then + FBDmgSourceText:SetText(format(FBT("DMG_SRC"), FBDmg_SourceText())); + end end function FBDmg_UpdateSpellText() @@ -562,6 +569,12 @@ function FBDmg_BuildOptions() FBDmg_UpdateSourceText(); FBDmg_UpdateSpellText(); FBDmg_SyncOptions(); + -- beim Oeffnen des Fensters die Zielzeile auffrischen (Vor-Hook auf OnShow) + local prevShow = panel:GetScript("OnShow"); + panel:SetScript("OnShow", function() + if (prevShow) then prevShow(); end + FBDmg_UpdateSourceText(); + end); end function FBDmg_SyncOptions() diff --git a/FBHealBox_Raid.lua b/FBHealBox_Raid.lua index d556bfe..618e34e 100644 --- a/FBHealBox_Raid.lua +++ b/FBHealBox_Raid.lua @@ -725,6 +725,7 @@ end -- ========================================================================== function FBRaid_UpdateRoster() + FBRaidRosterDirty = false; local cfg = FBRaid_Cfg(); FBRaidUnitCell = {}; FBRaidMembers = 0; @@ -737,6 +738,8 @@ function FBRaid_UpdateRoster() if (FBRaidCells[g] and FBRaidCells[g][pos]) then local c = FBRaidCells[g][pos]; c.unit = nil; c.name = nil; c.ghost = nil; c.index = nil; + c.dispelKnown = nil; c.lastText = nil; c.lastMax = nil; c.colorKey = nil; c.manaShown = nil; c.lastMpMax = nil; + c.vHp = nil; c.vShield = nil; c.vInc = nil; c.vMp = nil; c:Hide(); end end @@ -897,7 +900,35 @@ end -- [ Zelleninhalt ] -- ========================================================================== -function FBRaid_UpdateCell(c) +-- Zellenfarbe / Maxima / Text nur bei Aenderung setzen +function FBRaid_SetColor(c, key, r, g, b, a) + if (c.colorKey == key) then return; end + c.colorKey = key; + c.HealthBar:SetStatusBarColor(r, g, b, a); +end + +function FBRaid_SetValues(c, hp, shieldTop, incTop) + if (c.vHp ~= hp) then c.vHp = hp; c.HealthBar:SetValue(hp); end + if (c.vShield ~= shieldTop) then c.vShield = shieldTop; c.ShieldBar:SetValue(shieldTop); end + if (c.vInc ~= incTop) then c.vInc = incTop; c.IncHealBar:SetValue(incTop); end +end + +function FBRaid_SetMax(c, hpMax) + if (c.lastMax == hpMax) then return; end + c.lastMax = hpMax; + c.HealthBar:SetMinMaxValues(0, hpMax); + c.ShieldBar:SetMinMaxValues(0, hpMax); + c.IncHealBar:SetMinMaxValues(0, hpMax); +end + +function FBRaid_SetText(c, text) + if (c.lastText == text) then return; end + c.lastText = text; + if (text == false) then c.HPText:Hide(); else c.HPText:SetText(text); c.HPText:Show(); end +end + +-- auraChanged: true bei UNIT_AURA, nur dann wird der Debuff neu gesucht +function FBRaid_UpdateCell(c, auraChanged) if (not c) or (not c.unit) or (not c:IsShown()) then return; end local cfg = FBRaid_Cfg(); local hp, hpMax = FBRaid_Health(c); @@ -908,60 +939,59 @@ function FBRaid_UpdateCell(c) local key = "STATE_DEAD"; if (state == "ghost") then key = "STATE_GHOST"; end if (state == "offline") then key = "STATE_OFFLINE"; end - c.HPText:SetText(FBT(key)); - c.HPText:Show(); - c.HealthBar:SetMinMaxValues(0, hpMax); c.HealthBar:SetValue(0); - c.HealthBar:SetStatusBarColor(0.5, 0.5, 0.5, 1); - c.ShieldBar:SetMinMaxValues(0, hpMax); c.ShieldBar:SetValue(0); - c.IncHealBar:SetMinMaxValues(0, hpMax); c.IncHealBar:SetValue(0); - c.ManaBar:Hide(); + FBRaid_SetText(c, FBT(key)); + FBRaid_SetMax(c, hpMax); + FBRaid_SetValues(c, 0, 0, 0); + FBRaid_SetColor(c, "state", 0.5, 0.5, 0.5, 1); + if (c.manaShown ~= false) then c.manaShown = false; c.ManaBar:Hide(); end return; end -- Leben und Schichten - c.HealthBar:SetMinMaxValues(0, hpMax); - c.HealthBar:SetValue(hp); + FBRaid_SetMax(c, hpMax); local inc, shield = FBRaid_Incoming(c); - local shieldTop = math.min(hpMax, hp + shield); - c.ShieldBar:SetMinMaxValues(0, hpMax); - c.ShieldBar:SetValue(shieldTop); - c.IncHealBar:SetMinMaxValues(0, hpMax); - c.IncHealBar:SetValue(math.min(hpMax, shieldTop + inc)); + local shieldTop = hp + shield; + if (shieldTop > hpMax) then shieldTop = hpMax; end + local incTop = shieldTop + inc; + if (incTop > hpMax) then incTop = hpMax; end + FBRaid_SetValues(c, hp, shieldTop, incTop); -- HP-Text if (cfg.HPText == "percent") then - c.HPText:SetText(format("%d%%", math.floor(hp / hpMax * 100))); - c.HPText:Show(); + FBRaid_SetText(c, math.floor(hp / hpMax * 100).."%"); elseif (cfg.HPText == "deficit") then local def = hpMax - hp; - if (def > 0) then c.HPText:SetText("-"..def); else c.HPText:SetText(""); end - c.HPText:Show(); + if (def > 0) then FBRaid_SetText(c, "-"..def); else FBRaid_SetText(c, ""); end else - c.HPText:Hide(); + FBRaid_SetText(c, false); end -- Mana local mp, mpMax, hasMana = FBRaid_Mana(c); if (cfg.ManaBar == 1 and hasMana and mpMax > 0) then - c.ManaBar:SetMinMaxValues(0, mpMax); - c.ManaBar:SetValue(mp); - c.ManaBar:Show(); - else - c.ManaBar:Hide(); + if (c.lastMpMax ~= mpMax) then c.lastMpMax = mpMax; c.ManaBar:SetMinMaxValues(0, mpMax); end + if (c.vMp ~= mp) then c.vMp = mp; c.ManaBar:SetValue(mp); end + if (c.manaShown ~= true) then c.manaShown = true; c.ManaBar:Show(); end + elseif (c.manaShown ~= false) then + c.manaShown = false; c.ManaBar:Hide(); end - -- Farbe: Dispel schlaegt HP-Stand + -- Farbe: Dispel schlaegt HP-Stand (Debuff nur bei Aura-Aenderung neu suchen) + if (auraChanged or (not c.dispelKnown) or c.ghost) then + c.dispelType = FBRaid_Dispel(c); + c.dispelKnown = true; + end local frac = hp / hpMax; - local dtype = FBRaid_Dispel(c); + local dtype = c.dispelType; if (dtype and FBDispelColors[dtype]) then local col = FBDispelColors[dtype]; - c.HealthBar:SetStatusBarColor(col[1], col[2], col[3], col[4]); + FBRaid_SetColor(c, dtype, col[1], col[2], col[3], col[4]); elseif (frac > LowHP) then - c.HealthBar:SetStatusBarColor(0, 1, 0, 1); + FBRaid_SetColor(c, "green", 0, 1, 0, 1); elseif (frac > VeryLowHP) then - c.HealthBar:SetStatusBarColor(1, 0.9, 0, 1); + FBRaid_SetColor(c, "yellow", 1, 0.9, 0, 1); else - c.HealthBar:SetStatusBarColor(1, 0, 0, 1); + FBRaid_SetColor(c, "red", 1, 0, 0, 1); end end @@ -987,7 +1017,8 @@ end -- Wer wird angegriffen (Hook "Aggro" des Kerns, tt = "targettarget" oder nil) function FBRaid_CheckAggro(tt) - if (not FBRaid_IsActive()) then return; end + if (not FBRaid_IsActive()) then return false; end + local any = false; for g = 1, FBRAID_GROUPS do if (FBRaidCells[g]) then for pos = 1, FBRAID_PER_GROUP do @@ -996,16 +1027,18 @@ function FBRaid_CheckAggro(tt) local flag = false; if (HealBox.AggroMark == 1 and c:IsShown()) then if (c.ghost) then flag = (c.ghost.aggro == true); - elseif (tt) then flag = FBHealBox_UnitIsAggro(c.unit, tt); end + elseif (tt) then flag = FBHealBox_UnitIsAggroFast(c.unit, c.name, tt); end end if (c.underAttack ~= flag) then c.underAttack = flag; FBRaid_ApplyBorder(c); end + if (flag) then any = true; end end end end end + return any; end -- Platz links neben jeder Zelle fuer die Buff-Icons (aussen links) @@ -1042,13 +1075,17 @@ function FBRaid_UpdateSpellTimers(now) for pos = 1, FBRAID_PER_GROUP do local c = FBRaidCells[g][pos]; if (c) then - local shown = on and c.unit and c:IsShown(); + -- ohne verfolgten HoT/Schild auf der Einheit nur einmal ausblenden + local shown = on and c.unit and c:IsShown() and (c.ghost or FBHoTs[c.name] or FBShields[c.name]); + if (not shown and not c.timersShown) then shown = nil; end + c.timersShown = shown and true or false; + if (shown == nil) then shown = false; end for i = 1, FBRAID_MAX_BUTTONS do local b = c.buttons[i]; if (shown and b:IsShown()) then - local text, color = FBHealBox_SpellTimerFor(b.spellName, c.name, now, c.ghost, i); - if (not text and b.spellNameR) then - text, color = FBHealBox_SpellTimerFor(b.spellNameR, c.name, now, nil, i); + local text, color = FBHealBox_SpellTimerFor(b.spellBase, c.name, now, c.ghost, i); + if (not text and b.spellBaseR) then + text, color = FBHealBox_SpellTimerFor(b.spellBaseR, c.name, now, nil, i); end FBHealBox_SetButtonTimer(b, text, color); else @@ -1067,7 +1104,27 @@ function FBRaid_UpdateCooldowns() for pos = 1, FBRAID_PER_GROUP do local c = FBRaidCells[g][pos]; if (c) then - for i = 1, FBRAID_MAX_BUTTONS do FBHealBox_UpdateButtonCooldown(c.buttons[i]); end + for i = 1, FBRAID_MAX_BUTTONS do + local b = c.buttons[i]; + b.cdKey = nil; + FBHealBox_UpdateButtonState(b, "SPELL_UPDATE_COOLDOWN"); + end + end + end + end + end +end + +-- Button-Zustaende der sichtbaren Zellen (Hook "ButtonStates") +function FBRaid_UpdateButtonStates(event) + if (not FBRaid_IsActive()) then return; end + local nBtn = FBRaid_Cfg().Buttons or 0; + for g = 1, FBRAID_GROUPS do + if (FBRaidCells[g]) then + for pos = 1, FBRAID_PER_GROUP do + local c = FBRaidCells[g][pos]; + if (c and c.unit and c:IsShown()) then + for i = 1, nBtn do FBHealBox_UpdateButtonState(c.buttons[i], event); end end end end @@ -1130,7 +1187,10 @@ function FBRaid_SyncButtons() b.subIcon:Hide(); end end - FBHealBox_UpdateButtonCooldown(b); + b.cdKey = nil; + b.colorState = nil; + if (b.spellName) then b.spellBase = FBPredict_SplitCast(b.spellName); else b.spellBase = nil; end + if (b.spellNameR) then b.spellBaseR = FBPredict_SplitCast(b.spellNameR); else b.spellBaseR = nil; end end end end @@ -1179,21 +1239,28 @@ FBRaidEventFrame:RegisterEvent("UNIT_MAXMANA"); FBRaidEventFrame:RegisterEvent("UNIT_DISPLAYPOWER"); FBRaidEventFrame:RegisterEvent("UNIT_AURA"); +FBRaidRosterDirty = false; + FBRaidEventFrame:SetScript("OnEvent", function() if (event == "RAID_ROSTER_UPDATE" or event == "PARTY_MEMBERS_CHANGED" or event == "PLAYER_ENTERING_WORLD") then - FBRaid_UpdateRoster(); - FBRaid_SyncButtons(); - FBRaid_RefreshBuffBorders(); + -- Salven (mehrere Roster-Events je Frame beim Zonen) auf einen + -- Durchlauf im naechsten Frame zusammenfassen + FBRaidRosterDirty = true; return; end if (FBRaidTestMode > 0) then return; end local c = FBRaidUnitCell[arg1]; if (not c) then return; end - FBRaid_UpdateCell(c); + FBRaid_UpdateCell(c, (event == "UNIT_AURA")); if (event == "UNIT_AURA") then FBRaid_UpdateBuffBorder(c); end end); FBRaidEventFrame:SetScript("OnUpdate", function() + if (FBRaidRosterDirty) then + FBRaidRosterDirty = false; + FBRaid_UpdateRoster(); + FBRaid_RefreshBuffBorders(); + end FBRaidTickAccum = FBRaidTickAccum + (arg1 or 0); if (FBRaidTickAccum < FBRAID_TICK) then return; end FBRaidTickAccum = 0; @@ -1204,8 +1271,8 @@ end); -- fuer den Raid-Test haengen wir uns an denselben Takt und fordern ihn an. FBRaidGhostAccum = 0; FBRaidEventFrame.ghostTicker = CreateFrame("Frame", nil, UIParent); +FBRaidEventFrame.ghostTicker:Hide(); -- laeuft nur im Raid-Test (siehe FBRaid_SetTest) FBRaidEventFrame.ghostTicker:SetScript("OnUpdate", function() - if (FBRaidTestMode == 0) then return; end FBRaidGhostAccum = FBRaidGhostAccum + (arg1 or 0); if (FBRaidGhostAccum < 0.2) then return; end FBRaidGhostAccum = 0; @@ -1223,6 +1290,8 @@ function FBRaid_SetTest(mode) mode = tonumber(mode) or 0; if (mode ~= 20 and mode ~= 40) then mode = 0; end FBRaidTestMode = mode; + -- Geister-Atmung nur im Test rechnen lassen + if (mode > 0) then FBRaidEventFrame.ghostTicker:Show(); else FBRaidEventFrame.ghostTicker:Hide(); end if (mode > 0) then FBRaid_BuildTestGhosts(mode); if (HealBox.Active ~= 1) then HealBox.Active = 1; end @@ -1476,9 +1545,10 @@ FBHealBox_RegisterHook("ButtonsChanged", function() FBRaid_SyncButtons(); return true; end); -FBHealBox_RegisterHook("Aggro", function(tt) FBRaid_CheckAggro(tt); return true; end); +FBHealBox_RegisterHook("Aggro", function(tt) return FBRaid_CheckAggro(tt); end); FBHealBox_RegisterHook("SpellTimers", function(now) FBRaid_UpdateSpellTimers(now); return true; end); FBHealBox_RegisterHook("Cooldowns", function() FBRaid_UpdateCooldowns(); return true; end); +FBHealBox_RegisterHook("ButtonStates", function(ev) FBRaid_UpdateButtonStates(ev); return true; end); FBHealBox_RegisterHook("BuffIcons", function() FBRaid_UpdateBuffIcons(); return true; end); FBHealBox_RegisterHook("Status", function() local cfg = FBRaid_Cfg(); diff --git a/FBHealBox_Ticker.lua b/FBHealBox_Ticker.lua index 0eb5f4c..9ef0b40 100644 --- a/FBHealBox_Ticker.lua +++ b/FBHealBox_Ticker.lua @@ -317,6 +317,8 @@ end -- Balken einsammeln: eigene Plakette und, falls vorhanden, eigene Raid-Zelle function FBTicker_CollectBars() + -- alte Funken verstecken, Zwischenspeicher neu + FBTicker_HideAll(); FBTicker.sparks = {}; if (FBHealBox1 and FBHealBox1.ManaBar) then table.insert(FBTicker.sparks, { bar = FBHealBox1.ManaBar, spark = FBTicker_SparkFor(FBHealBox1.ManaBar) }); @@ -333,21 +335,22 @@ end function FBTicker_HideAll() for _, e in ipairs(FBTicker.sparks) do - if (e.spark) then e.spark:Hide(); end + if (e.spark and e.shown ~= false) then e.shown = false; e.spark:Hide(); end end end -function FBTicker_Draw() +-- Torwaechter fuer das Zeichnen: wird nur bei Events und Optionswechseln +-- neu bewertet (Mana-Events, Gestaltwechsel, Schalter), nicht jeden Frame. +FBTicker.gate = false; +function FBTicker_UpdateGate() local cfg = FBTicker_Cfg(); - if (cfg.Enabled ~= 1) or (HealBox.Active ~= 1) or (not FBTicker_PlayerHasMana()) then - FBTicker_HideAll(); - return; - end - -- Volles Mana: nichts zu regenerieren, Funke aus - if (FBTicker_ManaFull()) then - FBTicker_HideAll(); - return; - end + FBTicker.gate = (cfg.Enabled == 1) and (HealBox.Active == 1) + and FBTicker_PlayerHasMana() and (not FBTicker_ManaFull()); + if (not FBTicker.gate) then FBTicker_HideAll(); end +end + +function FBTicker_Draw() + if (not FBTicker.gate) then return; end local frac, mode = FBTicker_Progress(GetTime()); if (not frac) then FBTicker_HideAll(); @@ -355,7 +358,7 @@ function FBTicker_Draw() end local col = FBTICK_COLOR_TICK; if (mode == "fsr") then col = FBTICK_COLOR_FSR; end - local w = cfg.SparkW or 2; + local w = FBTicker_Cfg().SparkW or 2; for _, e in ipairs(FBTicker.sparks) do local bar, s = e.bar, e.spark; @@ -364,20 +367,29 @@ function FBTicker_Draw() local bw = bar:GetWidth() or 0; local bh = bar:GetHeight() or 0; if (bw > w and bh > 0) then - s:SetWidth(w); - s:SetHeight(bh); + if (e.w ~= w or e.h ~= bh) then + e.w = w; e.h = bh; + s:SetWidth(w); + s:SetHeight(bh); + end if (e.mode ~= mode) then e.mode = mode; s:SetTexture(col[1], col[2], col[3], col[4]); end - s:ClearAllPoints(); - s:SetPoint("LEFT", bar, "LEFT", frac * (bw - w), 0); - s:Show(); + -- Anker nur setzen, wenn der Funke mindestens einen halben + -- Pixel weitergewandert ist + local x = frac * (bw - w); + if (not e.x) or (math.abs(x - e.x) >= 0.5) then + e.x = x; + s:ClearAllPoints(); + s:SetPoint("LEFT", bar, "LEFT", x, 0); + end + if (not e.shown) then e.shown = true; s:Show(); end else - s:Hide(); + if (e.shown) then e.shown = false; s:Hide(); end end - else - s:Hide(); + elseif (e.shown) then + e.shown = false; s:Hide(); end end end @@ -396,17 +408,18 @@ FBTickerFrame:SetScript("OnEvent", function() if (event == "PLAYER_ENTERING_WORLD") then FBTicker_Reset(); FBTicker_CollectBars(); + FBTicker_UpdateGate(); return; end if (arg1 ~= "player") then return; end if (event == "UNIT_MANA") then FBTicker_OnManaChanged(); - if (FBTicker_ManaFull()) then FBTicker_HideAll(); end elseif (event == "UNIT_DISPLAYPOWER") then FBTicker_Reset(); else FBTicker.lastMana = UnitMana("player"); end + FBTicker_UpdateGate(); end); FBTickerFrame:SetScript("OnUpdate", function() FBTicker_Draw(); @@ -463,6 +476,7 @@ function FBTicker_BuildOptions() FBTickerEnabledCheck = FBHealBox_CreateCheck("FBHealBoxTickerEnabledCheck", tab, 40, y, "TICK_ENABLED", "TICK_ENABLED_TIP", function() FBTicker_Cfg().Enabled = FBTickerEnabledCheck:GetChecked() and 1 or 0; + FBTicker_UpdateGate(); FBTicker_Draw(); end); FBTickerFSRCheck = FBHealBox_CreateCheck("FBHealBoxTickerFSRCheck", tab, 250, y, "TICK_FSR", "TICK_FSR_TIP", function() @@ -514,8 +528,8 @@ end); FBHealBox_RegisterHook("Defaults", function() return FBTicker_ApplyDefaults(); end); FBHealBox_RegisterHook("SyncOptions", function() return FBTicker_SyncOptions(); end); FBHealBox_RegisterHook("ApplyLocale", function() return FBTicker_ApplyLocale(); end); -FBHealBox_RegisterHook("UpdateNames", function() FBTicker_CollectBars(); return true; end); -FBHealBox_RegisterHook("ActiveToggle", function() FBTicker_Draw(); return true; end); +FBHealBox_RegisterHook("UpdateNames", function() FBTicker_CollectBars(); FBTicker_UpdateGate(); return true; end); +FBHealBox_RegisterHook("ActiveToggle", function() FBTicker_UpdateGate(); FBTicker_Draw(); return true; end); FBHealBox_RegisterHook("Status", function() local state = FBT("TICK_UNSYNCED"); local nextIn = 0; @@ -535,6 +549,7 @@ FBHealBox_RegisterHook("Slash", function(msg) local cfg = FBTicker_Cfg(); if (cfg.Enabled == 1) then cfg.Enabled = 0; else cfg.Enabled = 1; end FBTicker_SyncOptions(); + FBTicker_UpdateGate(); FBTicker_Draw(); local key = "TICK_OFF"; if (cfg.Enabled == 1) then key = "TICK_ON"; end