From d429360d34aefb920e3132a08d54ebb8c99fd217 Mon Sep 17 00:00:00 2001 From: Mquadrat Date: Wed, 14 Oct 2026 21:21:16 +0200 Subject: [PATCH] Add files via upload --- CHANGELOG.md | 49 ++++++++++++++++ FBHealBox.lua | 159 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 184 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8a21ce..cbb88f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # Changelog +## 1.4.7.1 (2026-10-14) + +Fixes from an external review of 1.4.7. + +### English + +**Fixed** + +- **Buff icons:** one icon per buff and its group version (`FBBuffAlternates`), showing the version the unit carries. Before, each version also matched the other's texture, so Fortitude could show twice. Mark and Gift of the Wild share a texture: on yourself the buff name decides, on others your latest own cast. +- **Buff icons:** only buffs on one of your buttons get an icon (left side always, right side with the right-click spell on), as documented. `/fbp buffs` shows which ones do. +- **Own buff timer:** recasting the group version on yourself resets the timer again. +- **Line of sight:** a click during a cast no longer erases the cast's line of sight check; an error at the end of the cast marks the right unit (`FBLOSCast`, `FBLOSClick`). Clicks held by a client spell queue (Nampower) keep their check; an unanswered click no longer claims a later cast from the action bar. +- **Smart Healing:** Heal, Greater Heal, Healing Touch and Healing Wave weight +healing by the base cast time before talents (`FBBaseCastTime`). Unlearned ranks got about a seventh too little bonus. +- **Smart Healing:** `FBSmartSkip` and `FBRankLearnLevel` also know the German spell names. +- **Crit heals:** crit lines close their cast and are still not learned. A following smaller rank is no longer learned as the button's rank. +- **Prayer of Healing:** the prediction shows on every group member HealComm gets, not only on the clicked unit. Every line of the cast counts towards learning. +- **Smart Damage:** finds Arcane Shot and Flame Shock (tooltip form "causes X damage"). Aimed Shot left the hunter list. +- **Smart Damage:** learns from plain hits only; absorbed, blocked and vulnerability-boosted hits are ignored. +- **Raid grid:** cells pick up names that load late (`UNIT_NAME_UPDATE`). + +**Good to know** + +- Buffs on none of your buttons no longer get an icon. Put them on a button to see them. +- With the matching talents, first casts of unlearned ranks of the four spells above are estimated slightly higher. +- Smart Damage keeps its learned values. + +### Deutsch + +**Behoben** + +- **Buff-Icons:** ein Icon je Buff und seiner Gruppenversion (`FBBuffAlternates`), mit der Fassung, die die Einheit traegt. Vorher passte jede Fassung auch auf die Textur der anderen, Seelenstaerke konnte doppelt erscheinen. Mal und Gabe der Wildnis teilen sich eine Textur: bei dir entscheidet der Buffname, bei anderen dein letzter eigener Cast. +- **Buff-Icons:** nur Buffs auf einem deiner Buttons bekommen ein Icon (links immer, rechts mit eingeschaltetem Rechtsklick-Zauber), wie beschrieben. `/fbp buffs` zeigt, welche. +- **Eigene Buff-Uhr:** ein Neucast der Gruppenversion auf dich stellt die Uhr wieder zurueck. +- **Sichtlinie:** ein Klick waehrend eines Casts loescht dessen Sichtlinien-Pruefung nicht mehr; eine Meldung am Castende markiert die richtige Einheit (`FBLOSCast`, `FBLOSClick`). Klicks in der Zauberwarteschlange eines Clientmods (Nampower) behalten ihre Pruefung; ein unbeantworteter Klick uebernimmt keinen spaeteren Cast von der Aktionsleiste mehr. +- **Smart Healing:** Heilen, Grosse Heilung, Heilende Beruehrung und Welle der Heilung gewichten +Heilung nach der Basis-Zauberzeit vor Talenten (`FBBaseCastTime`). Ungelernte Raenge bekamen rund ein Siebtel zu wenig Bonus. +- **Smart Healing:** `FBSmartSkip` und `FBRankLearnLevel` kennen auch die deutschen Zaubernamen. +- **Crit-Heilungen:** Crit-Zeilen schliessen ihren Cast ab und werden weiterhin nicht gelernt. Ein folgender kleinerer Rang wird nicht mehr als Rang des Buttons gelernt. +- **Gebet der Heilung:** die Vorhersage erscheint auf jedem Gruppenmitglied, das HealComm bekommt, nicht nur auf der angeklickten Einheit. Jede Zeile des Casts zaehlt zum Lernen. +- **Smart Damage:** findet Arkaner Schuss und Flammenschock (Tooltip-Form "causes X damage"). Gezielter Schuss ist aus der Jaegerliste raus. +- **Smart Damage:** lernt nur aus normalen Treffern; absorbierte, geblockte und durch Verwundbarkeit verstaerkte Treffer zaehlen nicht. +- **Raidraster:** Zellen uebernehmen spaet geladene Namen (`UNIT_NAME_UPDATE`). + +**Gut zu wissen** + +- Buffs auf keinem deiner Buttons bekommen kein Icon mehr. Leg sie auf einen Button, um sie zu sehen. +- Mit den passenden Talenten schaetzt das Addon die ersten Casts ungelernter Raenge der vier Zauber oben etwas hoeher. +- Smart Damage behaelt seine gelernten Werte. + + ## 1.4.7 (2026-10-08) The complete code review in one version. Fixes first: Weakened Soul finally shows, other healers' HoTs and shields no longer pass as yours, a click only counts for the cast it started, direct heals are learned, the tooltip scanner reads cast time and range where they actually are, Smart Healing leaves group heals and cooldown spells alone, Smart Damage's minimum damage is safe again, plus line of sight, localized clients, the scale slider and drag and drop in the raid grid. Then performance, robustness and clean-up: redraws collected once per frame, mana events only touch the mana strip, no generic globals any more, modules independent of each other, errors reported instead of swallowed. diff --git a/FBHealBox.lua b/FBHealBox.lua index caddaea..c874132 100644 --- a/FBHealBox.lua +++ b/FBHealBox.lua @@ -3416,15 +3416,26 @@ end 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 - 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 +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 + +-- Name eines eigenen Buffs ueber den Scan-Tooltip (id aus GetPlayerBuff) +function FBHealBox_PlayerBuffName(id) + if (not FBPredictTip) or (not FBPredictTip.SetPlayerBuff) then return nil; end + FBPredictTip:SetOwner(UIParent, "ANCHOR_NONE"); + FBPredictTip:ClearLines(); + FBPredictTip:SetPlayerBuff(id); + 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) @@ -3683,10 +3694,12 @@ FBLOSFlags = {}; -- [Name] = Ablaufzeit der Markierung (Fallback-Weg) -- Start eines Casts und noch einmal an seinem Ende. Deshalb zwei Plaetze: -- -- FBLOSClick: das Ziel des letzten Klicks auf einen Button, bis der Server --- ihn beantwortet hat { name, spell, untilT }. Laeuft der geklickte Zauber --- mit Zauberzeit an, wandert das Ziel nach FBLOSCast. Ein Sofortzauber, der --- durchgeht (SPELLCAST_STOP), ein anderer Zauber, der anlaeuft, oder eine --- Fehlermeldung, die den Klick beantwortet, raeumt ihn ab. +-- ihn beantwortet hat { name, spell, t, untilT }. Laeuft der geklickte +-- Zauber mit Zauberzeit an, wandert das Ziel nach FBLOSCast. Ein +-- Sofortzauber, der durchgeht (SPELLCAST_STOP), ein anderer Zauber, der +-- anlaeuft, oder eine Fehlermeldung, die den Klick beantwortet, raeumt ihn +-- ab. Das Ende eines laufenden Casts nicht: Ein Klick ohne Antwort waehrend +-- des Casts ist vorgemerkt und geht erst danach hinaus (FBLOS_CastDone). -- FBLOSCast: das Ziel des laufenden Zaubers mit Zauberzeit bis Castende plus -- Spielraum { name, endT, untilT, seq }. Ein weiterer Klick waehrend des -- Casts legt nur einen neuen FBLOSClick an, und seine Antwort ("Another @@ -3773,18 +3786,23 @@ end -- dessen Spielraum fuer eine spaete Meldung endet dann hier. function FBLOS_NoteClick(targetName, spell, now) if (FBLOSCast and now > FBLOSCast.endT) then FBLOSCast = nil; end - FBLOSClick = { name = targetName, spell = spell, untilT = now + FBLOS_ERROR_WINDOW }; + FBLOSClick = { name = targetName, spell = spell, t = now, untilT = now + FBLOS_ERROR_WINDOW }; end -- SPELLCAST_START: Laeuft der geklickte Zauber an, wird sein Ziel zum -- Kandidaten des laufenden Casts; ein anderer Zauber gehoert nicht zu dem --- Klick. Ein neuer Castbeginn beendet in jedem Fall den vorigen Cast. +-- Klick, ein Klick von vor mehr als FBPREDICT_CLICK_TIME Sekunden auch +-- nicht (dieselbe Frist wie FBPredict_TakeClick). Sonst uebernahm ein Klick, +-- der ohne Antwort verpufft war, Sekunden spaeter denselben Zauber von der +-- Aktionsleiste auf ein ganz anderes Ziel. Ein neuer Castbeginn beendet in +-- jedem Fall den vorigen Cast. function FBLOS_CastStart(spell, castMs) FBLOSCast = nil; local c = FBLOSClick; FBLOSClick = nil; - if (c and spell == c.spell) then - local endT = GetTime() + (castMs or 0) / 1000; + local now = GetTime(); + if (c and spell == c.spell and now <= c.t + FBPREDICT_CLICK_TIME) then + local endT = now + (castMs or 0) / 1000; FBLOSCast = { name = c.name, endT = endT, untilT = endT + FBLOS_ERROR_WINDOW, seq = FBPredictClickSeq }; end end @@ -3798,13 +3816,34 @@ function FBLOS_CastDelayed(ms) c.untilT = c.untilT + d; end --- SPELLCAST_STOP (der Cast ist durchgegangen, die Sicht war da) und -- SPELLCAST_INTERRUPTED: beide Kandidaten sind beantwortet function FBLOS_Reset() FBLOSClick = nil; FBLOSCast = nil; end +-- SPELLCAST_STOP: Ein Zauber ist durchgegangen, die Sicht war da. Lief ein +-- Zauber mit Zauberzeit, ist das sein Ende. Ein Klick, der waehrenddessen +-- kam und keine Antwort bekam, ist ein vorgemerkter naechster Versuch (die +-- Zauberwarteschlange eines Clientmods wie Nampower); ohne Warteschlange +-- haette ihn "Another action is in progress" schon abgeraeumt. Er geht erst +-- jetzt hinaus, also beginnen seine Fristen hier, und er bleibt Kandidat +-- fuer den naechsten Castbeginn, so wie FBPredictClick bei +-- FBPredict_ClickDone. Ohne laufenden Cast war es ein Sofortzauber, und der +-- Klick ist beantwortet. Raeumte das Castende beide ab, markierte der +-- vorgemerkte Zauber bei fehlender Sicht niemanden. +function FBLOS_CastDone() + local now = GetTime(); + local cast, click = FBLOSCast, FBLOSClick; + FBLOSCast = nil; + if (cast and now <= cast.untilT and click) then + click.t = now; + click.untilT = now + FBLOS_ERROR_WINDOW; + return; + end + FBLOSClick = nil; +end + -- Fehlermeldung "nicht in Sichtlinie": das Ziel markieren, dem die Meldung -- gilt. Frueher fiel die Zuordnung auf das letzte Button-Ziel der letzten -- zwei Sekunden, das freundliche Ziel oder den Spieler selbst zurueck. Ein @@ -6092,10 +6131,17 @@ function FBPredict_ConfirmBuffRefresh() end end --- Buff-Laufzeit auf einer Einheit vermerken +-- Buff-Laufzeit auf einer Einheit vermerken. Einzel- und Gruppenbuff +-- ersetzen einander, die Einheit traegt also nur noch diese Variante: Die +-- Uhr der anderen (FBBuffAlternates) faellt weg. Liefe sie weiter, gaelte +-- sie bei Mal und Gabe der Wildnis nie als verschwunden, denn beide haben +-- dieselbe Textur: Nach Gabe der Wildnis auf ein Ziel mit eigenem Mal +-- zeigte das Icon weiter das Mal mit seiner alten Restzeit. 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 + local alt = FBBuffAlternates[spellName]; + if (alt) then FBBuffTimers[unitName][alt] = nil; end local t = FBBuffTimers[unitName][spellName]; if (t) then t.expires = GetTime() + secs; else FBBuffTimers[unitName][spellName] = { expires = GetTime() + secs }; end FBBuffIconsDirty = true; @@ -6108,6 +6154,7 @@ end -- je Sekunde. Jetzt wird die Liste einmal je Frame gelesen und alle Wachen -- bedienen sich daraus; der Zeitstempel arbeitet wie bei FBHealBox_UnitBuffs. FBPlayerBuffLeft = {}; +FBPlayerBuffIndex = {}; -- [TEXTUR] = Buff-Index dazu (fuer den Namen im Tooltip) FBPlayerBuffStamp = nil; function FBPredict_ScanPlayerBuffTimes() @@ -6115,6 +6162,7 @@ function FBPredict_ScanPlayerBuffTimes() if (FBPlayerBuffStamp == now) then return FBPlayerBuffLeft; end FBPlayerBuffStamp = now; for k in pairs(FBPlayerBuffLeft) do FBPlayerBuffLeft[k] = nil; end + for k in pairs(FBPlayerBuffIndex) do FBPlayerBuffIndex[k] = nil; end if (not GetPlayerBuff) or (not GetPlayerBuffTexture) or (not GetPlayerBuffTimeLeft) then return FBPlayerBuffLeft; end @@ -6130,6 +6178,7 @@ function FBPredict_ScanPlayerBuffTimes() -- gleiche Textur mehrfach (etwa gestapelte Buffs): laengste Zeit if (not FBPlayerBuffLeft[up]) or (left > FBPlayerBuffLeft[up]) then FBPlayerBuffLeft[up] = left; + FBPlayerBuffIndex[up] = id; end end end @@ -6144,6 +6193,59 @@ function FBPredict_PlayerBuffTimeLeft(tex) return FBPredict_ScanPlayerBuffTimes()[tex]; end +-- Name des eigenen Buffs mit dieser Textur, per Scan-Tooltip gelesen. Je +-- Anwendung nur einmal: Solange das Ablaufende (jetzt + Restzeit) gleich +-- bleibt, ist es derselbe Buff; ein Neucast oder ein anderer Buff mit +-- derselben Textur verschiebt es und loest ein neues Lesen aus. +FBPlayerBuffNameMemo = {}; -- [TEXTUR] = { name, endT } +function FBPredict_PlayerBuffName(tex, left) + local endT = GetTime() + left; + local m = FBPlayerBuffNameMemo[tex]; + if (m and math.abs(m.endT - endT) < 1.5) then return m.name; end + if (not m) then m = {}; FBPlayerBuffNameMemo[tex] = m; end + local id = FBPlayerBuffIndex[tex]; + m.name = id and FBHealBox_PlayerBuffName(id); + m.endT = endT; + return m.name; +end + +-- Restlaufzeit eines eigenen Buffs, aber nur, wenn du genau diese Variante +-- traegst; sonst nil. Mal und Gabe der Wildnis haben dieselbe Textur, die +-- Restzeit allein trennt sie nicht: 1800 Sekunden sind ein frisches Mal +-- oder eine halb abgelaufene Gabe. Bekaemen beide die Uhr, spraenge das +-- Icon, sobald die Gabe unter 30 Minuten faellt, aufs Mal und wieder auf +-- ganz farbig. Den Ausschlag gibt deshalb der Name im Tooltip. Ist +-- die Gruppenversion nicht gelernt, ist ihre Textur unbekannt; auch dann +-- entscheidet der Name, damit eine fremde Gabe nicht als Mal mit Uhr gilt. +-- Ohne lesbaren Namen entscheidet die Dauer: eine Restzeit, die nur eine +-- Variante fasst, gehoert ihr; sonst bleibt die laufende Uhr, und ohne Uhr +-- gilt die kuerzere Variante. Zweiter Rueckgabewert true: Du traegst laut +-- Tooltip die andere Variante, eine Uhr dieser hier ist hinfaellig. +function FBPredict_PlayerBuffLeft(spellName, w) + local left = FBPredict_PlayerBuffTimeLeft(w.tex); + if (not left) then return nil; end + local alt = FBBuffAlternates[spellName]; + if (not alt) then return left; end + local aw = FBPredictWatch[alt]; + if (aw and aw.tex ~= w.tex) then return left; end -- eigene Textur: eindeutig + + local carried = FBPredict_PlayerBuffName(w.tex, left); + if (carried == spellName) then return left; end + if (carried == alt) then return nil, true; end + if (not aw) then return left; end + + local secs, altSecs = (w.buffSecs or 0), (aw.buffSecs or 0); + local fits, altFits = (secs + 1 >= left), (altSecs + 1 >= left); + if (fits and not altFits) then return left; end + if (altFits and not fits) then return nil; end + local timers = FBBuffTimers[UnitName("player")]; + local now = GetTime(); + if (timers and timers[spellName] and timers[spellName].expires > now) then return left; end + if (timers and timers[alt] and timers[alt].expires > now) then return nil; end + if (secs < altSecs) or (secs == altSecs and spellName < alt) then return left; end + return nil; +end + -- [ Lernspeicher (wandert in die SavedVariables) ] -------------------------- FBPREDICT_ABSORB_MAX_FACTOR = 1.5; @@ -6660,8 +6762,14 @@ function FBPredict_ScanUnit(unit) FBBuffPresent[name][spellName] = present; local tracked = FBBuffTimers[name] and FBBuffTimers[name][spellName]; if (present and unit == "player") then - local left = FBPredict_PlayerBuffTimeLeft(w.tex); - if (left) then FBPredict_StartBuff(name, spellName, left); end + -- nur die Variante, die du wirklich traegst (geteilte Textur) + local left, other = FBPredict_PlayerBuffLeft(spellName, w); + if (left) then + FBPredict_StartBuff(name, spellName, left); + elseif (other and tracked) then + FBBuffTimers[name][spellName] = nil; + FBBuffIconsDirty = true; + end elseif (tracked and not present) then FBBuffTimers[name][spellName] = nil; end @@ -7090,7 +7198,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. - FBLOS_Reset(); -- Cast ging durch, die Sicht war da + FBLOS_CastDone(); -- Cast ging durch, die Sicht war da FBPredict_ClickDone(); FBPredict_ConfirmBuffRefresh(); FBPredict_CastEnd(true); @@ -8077,14 +8185,17 @@ function FBPredict_RefreshPlayerBuffs(elapsed) for _, spellName in ipairs(FBPredictBuffSpells) do if (FBBuffPresent[me][spellName]) then local w = FBPredictWatch[spellName]; - local left = FBPredict_PlayerBuffTimeLeft(w.tex); + local left, other = FBPredict_PlayerBuffLeft(spellName, w); + local t = timers and timers[spellName]; 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 + elseif (other and t) then + timers[spellName] = nil; + FBBuffIconsDirty = true; end end end