3 Commits
Author SHA1 Message Date
Mquadrat fac63b2d19 Add files via upload 2026-10-07 22:48:10 +02:00
Mquadrat b6db62d7dc Update README.md 2026-10-06 10:09:24 +02:00
Mquadrat 934e0047b0 Add files via upload 2026-10-06 10:08:02 +02:00
5 changed files with 544 additions and 140 deletions
+37 -2
View File
@@ -1,8 +1,43 @@
# Changelog
## 1.4.6 (2026-10-06)
Performance pass over the display loop, with no change in behaviour: only the units that actually changed are redrawn, and two loops that ran empty five times a second now stop early.
### English
**Performance**
- **Only the units that changed are redrawn.** The 0.2 second pass used to set a single dirty flag: if any heal over time ticked anywhere, everything was redrawn, which in a full raid meant forty cells plus ten plates although exactly one unit had changed. The pass now collects the affected unit names in a reused table and hands them to `FBHealBox_RefreshUnitsByName`, and the raid module picks up the same list through the new `RefreshNames` hook. With several HoTs running this turns three to five full sweeps a second into at most one plus a handful of single-unit updates.
- **A full sweep stays as a safety net.** At most once a second, and only while something is actually changing, the old full refresh still runs, so a name that does not match a plate for any reason can never leave a stale bar behind for longer than that.
- **Empty leftovers are cleared.** When the last heal over time or the last foreign heal on a unit expires, the now-empty sub-table is removed instead of being left behind. Several places read `FBHoTs[name]` as "is anything running on this unit", and an empty table answered yes.
- **Raid spell timers skip cells with nothing to show.** The intent to skip was already in the code but the result was thrown away two lines later, so the button loop ran anyway: in a full raid that was 160 do-nothing calls five times a second. The cell is now skipped outright, exactly as the plates already did.
- **Spell timers get an early out.** When no heal over time and no shield of yours is running anywhere, the plate loop no longer asks every slot for its unit and name five times a second; it only looks where a timer still has to be cleared. Classes without HoTs and the time between fights benefit.
**Checked and left alone**
- The three `OnUpdate` loops do no unnecessary work. The prediction checks three dirty flags and two counters per frame, the mana ticker leaves at its gate (a full mana bar means no pass at all), and the raid ghost ticker is hidden outside test mode. Event handling filters unit events through a lookup table rather than walking every frame.
- The global cooldown has been drawn as a sweep since 1.4.5.1, which means every visible button gets a fresh sweep after every cast. That is the intended behaviour and the animation runs in the client, not in Lua, so it stays. Should it ever feel heavy in a dense raid, `FBCD_SHOW_MIN` set to 2 limits the sweep to real cooldowns again.
### Deutsch
**Leistung**
- **Nur noch die Einheiten neu zeichnen, bei denen sich etwas geaendert hat.** Der 0,2-Sekunden-Takt setzte bisher ein einziges Dirty-Flag: Lief irgendwo ein HoT-Tick ab, wurde alles neu gezeichnet, im Vierzigerraid also vierzig Zellen und zehn Plaketten, obwohl sich bei genau einer Einheit etwas geaendert hatte. Der Takt sammelt die betroffenen Namen jetzt in einer wiederverwendeten Tabelle und reicht sie an `FBHealBox_RefreshUnitsByName`; das Raidmodul bekommt dieselbe Liste ueber den neuen Hook `RefreshNames`. Mit mehreren laufenden HoTs werden aus drei bis fuenf vollen Durchlaeufen je Sekunde hoechstens einer plus eine Handvoll Einzel-Updates.
- **Der volle Durchlauf bleibt als Sicherheitsnetz.** Hoechstens einmal je Sekunde, und nur solange sich ueberhaupt etwas aendert, laeuft er weiterhin. Ein Name, der aus irgendeinem Grund zu keiner Plakette passt, kann so nie laenger als eine Sekunde einen alten Balken stehen lassen.
- **Leere Reste werden weggeraeumt.** Laeuft der letzte HoT oder die letzte fremde Heilung auf einer Einheit ab, verschwindet die nun leere Untertabelle, statt liegen zu bleiben. Mehrere Stellen lesen `FBHoTs[name]` als "laeuft da ueberhaupt etwas", und eine leere Tabelle hat darauf mit ja geantwortet.
- **Raid-Zaubertimer ueberspringen Zellen ohne Anzeige.** Die Absicht zu ueberspringen stand schon im Code, das Ergebnis wurde aber zwei Zeilen spaeter wieder verworfen, sodass die Buttonschleife trotzdem lief: im Vierzigerraid 160 Leeraufrufe fuenfmal je Sekunde. Die Zelle wird jetzt wirklich uebersprungen, genau wie es die Plaketten laengst machen.
- **Zaubertimer steigen frueher aus.** Laeuft nirgends ein eigener HoT und kein eigener Schild, fragt die Plakettenschleife nicht mehr fuenfmal je Sekunde jeden Slot nach Einheit und Namen, sondern schaut nur noch dort hinein, wo ein Timer wegzuraeumen ist. Davon profitieren Klassen ohne HoTs und die Zeit zwischen den Kaempfen.
**Geprueft und so gelassen**
- Die drei `OnUpdate`-Schleifen arbeiten nicht unnoetig. Die Vorhersage prueft je Frame drei Dirty-Flags und zwei Zaehler, der Mana-Ticker steigt am Torwaechter aus (voller Manabalken heisst gar kein Durchlauf), und der Geister-Ticker im Raid ist ausserhalb des Testmodus versteckt. Die Ereignisverarbeitung filtert Unit-Events ueber eine Nachschlagetabelle, statt je Frame zu suchen.
- Der globale Cooldown laeuft seit 1.4.5.1 als Uhr mit, das heisst nach jedem Zauber bekommt jeder sichtbare Button eine neue Uhr gesetzt. Das ist das gewollte Verhalten, und die Animation laeuft im Client, nicht in Lua, deshalb bleibt es so. Sollte es im dichten Raid doch schwer wirken, beschraenkt `FBCD_SHOW_MIN` auf 2 die Uhr wieder auf echte Abklingzeiten.
## 1.4.5.3 (2026-10-05)
A readability option suggested by a user has been added: the area behind the bars can be given a solid background, so the game world no longer shows through where health is missing.
A readability option suggested by a user: the area behind the bars can be given a solid background, so the game world no longer shows through where health is missing.
### English
@@ -24,7 +59,7 @@ A readability option suggested by a user has been added: the area behind the bar
- **Wo die Flaeche haengt.** Die Textur sitzt auf der untersten der vier gestapelten Balken, der Heilvorhersage, auf der Ebene `BACKGROUND`. Damit liegt sie unter Vorhersage, Schild, Leben und Mana, und sie wird zusammen mit ihnen ein- und ausgeblendet; auf einer leeren oder im Anheftmodus versteckten Plakette bleibt also nichts stehen. Bei 0 Prozent wird gar keine Textur gesetzt statt einer voellig durchsichtigen.
- **Raidzellen brauchen nichts.** Sie haben bereits ihren eigenen Hintergrund mit 80 Prozent Schwarz, dort scheint keine Welt durch; der Regler fasst bewusst nur die Plaketten an.
**Danke**
**Dank**
- Idee und ein lauffaehiger Patch kamen von einem Nutzer, dem der durchscheinende Balken schlecht lesbar ist. Der Patch benutzte `WHITE8X8` mit `SetVertexColor`; hier ausgeliefert wird `SetTexture(r, g, b, a)`, das in 1.12 direkt eine Vollfarbe erzeugt und genau das ist, was der Manastreifen ohnehin schon macht. Gleiches Ergebnis, ohne Texturdatei.
+446 -115
View File
@@ -44,6 +44,15 @@
-- anfliegende Heilung.
-- * v1.4.5.3: Einstellbarer Hintergrund hinter den Balken der Plaketten
-- (Regler "Balkenhintergrund", Standard 0 = wie bisher durchscheinend).
-- * v1.4.6: Leistungsdurchgang ohne Funktionsaenderung: nur geaenderte
-- Einheiten werden neu gezeichnet (FBHealBox_RefreshUnitsByName, Hook
-- "RefreshNames"), Zaubertimer steigen frueher aus.
-- * v1.4.6.1: Fehler aus dem Code-Review: Geschwaechte Seele sichtbar,
-- fremde HoTs und Schilde nicht mehr als eigene, Tooltipscanner liest
-- Reichweite und Zauberzeit an der richtigen Stelle, Smart Healing laesst
-- Gruppenheilungen und Abklingzeiten in Ruhe und rechnet niedrige Raenge
-- richtig, Sichtlinie nur fuer das geklickte Ziel, Klassentabellen auf
-- jedem Client, Skalierung ohne Wandern, Drag & Drop im Raid.
--
-- Ehre wem Ehre gebuehrt: Aufbau, Namensplaketten und Grundidee stammen
-- aus dem Original.
@@ -77,6 +86,22 @@ do
if (eng) then FBClassToken = strupper(eng); end
end
-- Interner Klassenname immer englisch, egal in welcher Sprache der Client
-- laeuft. UnitClass liefert zuerst den lokalisierten Namen ("Priester" auf
-- deDE), alle Klassentabellen (Zauberlisten, Buff-Wache, Dispelfarben,
-- Klassenicon, Smart Damage) sind aber nach "Priest" usw. geschluesselt.
-- Ohne diese Abbildung blieben sie auf nicht englischen Clients leer, die
-- Dispelfaerbung fiel dort komplett aus. Fuer Texte im Chat und im
-- Optionsfenster bleibt der lokalisierte Name in FBClassLocal.
FBClassLocal = FBClass;
FBClassByToken = {
PRIEST = "Priest", DRUID = "Druid", PALADIN = "Paladin", SHAMAN = "Shaman",
MAGE = "Mage", WARLOCK = "Warlock", HUNTER = "Hunter", WARRIOR = "Warrior", ROGUE = "Rogue",
};
if (FBClassToken and FBClassByToken[FBClassToken]) then
FBClass = FBClassByToken[FBClassToken];
end
-- Rueckfall, wenn der Client kein englisches Token liefert: angezeigter
-- Klassenname. Bewusst in mehreren Sprachen, damit die Sperre auch auf
-- lokalisierten Clients greift.
@@ -159,7 +184,7 @@ HealBox = {
-- feuert ADDON_LOADED fuer uns.
FBADDON_NAME = "Heal Box Vanilla";
FBADDON_FOLDER = "FBHealBox";
HealBoxVersion = "|cFFFFFF00v1.4.5.3|r";
HealBoxVersion = "|cFFFFFF00v1.4.6.1|r";
-- ==========================================================================
-- [ Lokalisierung / Localization ]
@@ -206,7 +231,7 @@ FBLocale["enUS"] = {
SMARTRANK = "Smart Healing",
SMARTRANK_TIP = "Automatically casts the lowest spell rank that covers the target's missing health (minus incoming heals) plus safety margin.\n\n"
.. "|cFFFFD100Rules:|r\n"
.. "• Direct heals only (HoTs and shields untouched)\n"
.. "• Single-target direct heals only (HoTs, shields, group heals and spells with a cooldown untouched)\n"
.. "• A HoT on the target is not counted as incoming\n"
.. "• Always casts assigned rank below 30 % health\n"
.. "• Never heals more than the assigned rank\n"
@@ -307,6 +332,7 @@ FBLocale["enUS"] = {
DBG_HEAL = "Heal %s = %d (estimate now %d)",
DBG_ABSORB = "Absorb %d on %s (%d left)",
DBG_SMART_HOT = "Smart Healing skipped: %s is a heal over time",
DBG_SMART_SKIP = "Smart Healing skipped: %s is a group heal or has a cooldown",
FBP_SMART_CROSS = "· across spells: %s",
SMART_CROSS_ON = "Smart Healing may now switch spell within a heal chain (e.g. Greater Heal to Lesser Heal). |cFF00FF00On|r.",
SMART_CROSS_OFF = "Smart Healing now stays within the assigned spell and only lowers its rank. |cFFFF0000Cross-spell off|r.",
@@ -358,7 +384,7 @@ FBLocale["deDE"] = {
SMARTRANK = "Smart Healing",
SMARTRANK_TIP = "Wirkt automatisch den niedrigsten Zauberrang, der das fehlende Leben (abzgl. eingehender Heilung) plus Sicherheitsaufschlag deckt.\n\n"
.. "|cFFFFD100Regeln:|r\n"
.. "• Nur Direktheilung (HoTs und Schilde unberuehrt)\n"
.. "• Nur Direktheilung auf ein Ziel (HoTs, Schilde, Gruppenheilungen und Zauber mit Abklingzeit unberuehrt)\n"
.. "• Laufender HoT zaehlt nicht als anfliegende Heilung\n"
.. "• Unter 30 % Leben immer der belegte Rang\n"
.. "• Nie mehr Heilung als der belegte Rang\n"
@@ -459,6 +485,7 @@ FBLocale["deDE"] = {
DBG_HEAL = "Heilung %s = %d (Schaetzung jetzt %d)",
DBG_ABSORB = "Absorb %d auf %s (Rest %d)",
DBG_SMART_HOT = "Smart Healing uebersprungen: %s ist Heilung ueber Zeit",
DBG_SMART_SKIP = "Smart Healing uebersprungen: %s ist eine Gruppenheilung oder hat eine Abklingzeit",
FBP_SMART_CROSS = "· ueber Zaubergrenzen: %s",
SMART_CROSS_ON = "Smart Healing darf den Zauber innerhalb einer Heilkette wechseln (z. B. Grosse Heilung zu Geringem Heilen). |cFF00FF00An|r.",
SMART_CROSS_OFF = "Smart Healing bleibt beim belegten Zauber und senkt nur dessen Rang. |cFFFF0000Kettenwechsel aus|r.",
@@ -535,7 +562,7 @@ FBLocale["esES"] = {
SMARTRANK = "Smart Healing",
SMARTRANK_TIP = "Lanza automáticamente el rango más bajo que cubra la vida faltante (menos curaciones entrantes) más el margen de seguridad.\n\n"
.. "|cFFFFD100Reglas:|r\n"
.. "• Solo curaciones directas (HoTs y escudos intactos)\n"
.. "• Solo curaciones directas de un objetivo (HoTs, escudos, curaciones de grupo y hechizos con reutilización intactos)\n"
.. "• Un HoT activo no cuenta como curación entrante\n"
.. "• Siempre el rango asignado bajo 30 % de vida\n"
.. "• Nunca cura más que el rango asignado\n"
@@ -630,6 +657,7 @@ FBLocale["esES"] = {
DBG_HEAL = "Curación %s = %d (estimación ahora %d)",
DBG_ABSORB = "Absorción %d en %s (quedan %d)",
DBG_SMART_HOT = "Smart Healing omitido: %s es curación con el tiempo",
DBG_SMART_SKIP = "Smart Healing omitido: %s es una curación de grupo o tiene reutilización",
FBP_SMART_CROSS = "· entre hechizos: %s",
SMART_CROSS_ON = "Smart Healing puede cambiar de hechizo dentro de una cadena de curación (p. ej. Curar más y Curar menos). |cFF00FF00Activado|r.",
SMART_CROSS_OFF = "Smart Healing se queda en el hechizo asignado y solo baja su rango. |cFFFF0000Cambio de hechizo desactivado|r.",
@@ -678,7 +706,7 @@ FBLocale["frFR"] = {
SMARTRANK = "Smart Healing",
SMARTRANK_TIP = "Lance automatiquement le rang le plus bas couvrant la vie manquante (moins soins en cours) plus la marge de sécurité.\n\n"
.. "|cFFFFD100Règles :|r\n"
.. "• Soins directs uniquement (HoTs et boucliers intacts)\n"
.. "• Soins directs sur une cible uniquement (HoTs, boucliers, soins de groupe et sorts à temps de recharge intacts)\n"
.. "• Un HoT actif ne compte pas comme soin en cours\n"
.. "• Rang assigné conservé sous 30 % de vie\n"
.. "• Ne soigne jamais plus que le rang assigné\n"
@@ -773,6 +801,7 @@ FBLocale["frFR"] = {
DBG_HEAL = "Soin %s = %d (estimation maintenant %d)",
DBG_ABSORB = "Absorption %d sur %s (reste %d)",
DBG_SMART_HOT = "Smart Healing ignoré : %s est un soin sur la durée",
DBG_SMART_SKIP = "Smart Healing ignoré : %s est un soin de groupe ou a un temps de recharge",
FBP_SMART_CROSS = "· entre sorts : %s",
SMART_CROSS_ON = "Smart Healing peut changer de sort dans une chaîne de soins (p. ex. Soins supérieurs vers Soins inférieurs). |cFF00FF00Activé|r.",
SMART_CROSS_OFF = "Smart Healing reste sur le sort assigné et n'abaisse que son rang. |cFFFF0000Changement de sort désactivé|r.",
@@ -821,7 +850,7 @@ FBLocale["itIT"] = {
SMARTRANK = "Smart Healing",
SMARTRANK_TIP = "Lancia automaticamente il rango più basso la cui cura copre la salute mancante (meno cure in arrivo) più il margine di sicurezza.\n\n"
.. "|cFFFFD100Regole:|r\n"
.. "• Solo cure dirette (HoT e scudi non modificati)\n"
.. "• Solo cure dirette su un bersaglio (HoT, scudi, cure di gruppo e incantesimi con tempo di recupero non modificati)\n"
.. "• Un HoT attivo non conta come cura in arrivo\n"
.. "• Sotto il 30 % di salute lancia sempre il rango assegnato\n"
.. "• Mai una cura superiore al rango assegnato\n"
@@ -916,6 +945,7 @@ FBLocale["itIT"] = {
DBG_HEAL = "Cura %s = %d (stima ora %d)",
DBG_ABSORB = "Assorbimento %d su %s (restano %d)",
DBG_SMART_HOT = "Smart Healing saltato: %s è una cura nel tempo",
DBG_SMART_SKIP = "Smart Healing saltato: %s è una cura di gruppo o ha un tempo di recupero",
FBP_SMART_CROSS = "· tra incantesimi: %s",
SMART_CROSS_ON = "Smart Healing può cambiare incantesimo all'interno di una catena di cure (p. es. da Cura Superiore a Cura Inferiore). |cFF00FF00Attivo|r.",
SMART_CROSS_OFF = "Smart Healing resta sull'incantesimo assegnato e ne abbassa solo il rango. |cFFFF0000Cambio incantesimo disattivato|r.",
@@ -1426,7 +1456,7 @@ function FBHealBox_Announce()
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME.."|r "..HealBoxVersion.." : |cFF00FF00"..FBT("LOADED")..swowTag);
DEFAULT_CHAT_FRAME:AddMessage(FBT("CREDITS"));
if (FBClassBlocked) then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..format(FBT("CLASS_FORCED"), FBClass or "?"));
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..format(FBT("CLASS_FORCED"), FBClassLocal or FBClass or "?"));
end
FBHealBox_RunHook("Loaded"); -- Module melden sich im Chat
end
@@ -1463,7 +1493,7 @@ function FBHealBox_ApplyClassGate()
if (not FBGateAnnounced) then
FBGateAnnounced = true;
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME.."|r "..HealBoxVersion..": |cFFFF8000"
..format(FBT("CLASS_BLOCKED"), FBClass or "?").."|r");
..format(FBT("CLASS_BLOCKED"), FBClassLocal or FBClass or "?").."|r");
DEFAULT_CHAT_FRAME:AddMessage("|cFFAAAAAA"..FBT("CLASS_BLOCKED_HINT").."|r");
end
return false;
@@ -1669,10 +1699,18 @@ function FBLoadSpellData()
i = i + 1;
end
-- Alles, was je Zauberbuchplatz gemerkt wird, gilt nur fuer dieses
-- Zauberbuch: Ein neu gelernter Rang verschiebt die Plaetze dahinter.
-- Reichweite und Manapreis wurden frueher nie geleert und gehoerten nach
-- dem Lernen bis zum /reload zum falschen Zauber. Geleert wird vor der
-- Auswertung, damit FBPredict_BuildWatch die Speicher gleich wieder fuellt.
FBSpellNameCache = {};
FBSpellCastCache = {};
FBSpellRangeCache = {};
FBSpellCostCache = {};
FBSpellCDCache = {};
-- Tooltips auswerten: welche dieser Zauber sind HoTs bzw. Absorb-Schilde?
FBPredict_BuildWatch();
FBSpellNameCache = {};
FBSpellCastCache = {};
FBHealBox_InvalidateRangeSpell();
FBHealBox_ProbeAPIs();
-- Doppelt gezaehlte Absorb-Lernwerte (Versionen vor 1.4.2) verwerfen
@@ -2712,6 +2750,28 @@ for _, chain in ipairs(FBHealChains) do
end
end
-- Direktheilungen, die Smart Healing trotzdem nie abrangt. Bei Gruppen- und
-- Mehrzielheilungen sagt der Fehlbetrag der einen angeklickten Einheit nichts
-- ueber den Bedarf: Ein Gebet der Heilung ueber einem vollen Spieler wurde
-- bisher Rang 1, egal wie die Gruppe aussah. Bei Zaubern mit Abklingzeit
-- verbraucht ein kleiner Rang die volle Abklingzeit. Zauber mit Abklingzeit
-- erkennt FBHealBox_SmartSkipped zusaetzlich am Tooltip; die Liste deckt die
-- bekannten Faelle ab, auch wenn ein Tooltip keine Abklingzeit nennt.
FBSmartSkip = {
["Prayer of Healing"] = true, ["Chain Heal"] = true, ["Binding Heal"] = true,
["Circle of Healing"] = true, ["Prayer of Mending"] = true, ["Holy Nova"] = true,
["Holy Shock"] = true, ["Lay on Hands"] = true,
};
-- Laesst Smart Healing diesen Zauber in Ruhe?
function FBHealBox_SmartSkipped(base, ranks)
if (FBSmartSkip[base]) then return true; end
-- Abklingzeit laut Tooltip, laenger als der globale Cooldown
local top = ranks and ranks[table.getn(ranks)];
local cd = top and FBHealBox_SpellCooldownSecs(top.id);
return (cd ~= nil) and (cd > FBCD_MIN_DURATION);
end
-- Erwartete Heilung eines Rangs, oder nil, wenn der Zauber fuer Smart
-- Healing nicht in Frage kommt: kein Heilbetrag, Schild, HoT-Anteil, Buff
-- oder kein Heiltext im Tooltip. Gelernter Wert schlaegt den Tooltip.
@@ -2742,6 +2802,13 @@ function FBHealBox_SmartRank(castString, unit)
end
return castString;
end
-- Gruppenheilungen und Zauber mit Abklingzeit ebenso
if (FBHealBox_SmartSkipped(base, ranks)) then
if (FBPredictDebug) then
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..format(FBT("DBG_SMART_SKIP"), castString));
end
return castString;
end
if (not FBUnitExists(unit)) or FBTest_Ghost(unit) then return castString; end
local hp, hpMax = FBUnitHealth(unit);
@@ -2774,7 +2841,8 @@ function FBHealBox_SmartRank(castString, unit)
local bestSD, bestName, bestAmount = assignedSD, base, cap;
for _, spellName in ipairs(FBHealBox_HealFamily(base)) do
local list = FBPlayerSpells[spellName];
if (list) and ((spellName == base) or (not FBHealBox_IsHoT(spellName, list))) then
if (list) and ((spellName == base) or ((not FBHealBox_IsHoT(spellName, list))
and (not FBHealBox_SmartSkipped(spellName, list)))) then
for _, sd in ipairs(list) do
local amount = FBHealBox_DirectAmount(spellName, sd);
if (amount) and (amount >= need) and (amount < bestAmount) then
@@ -2968,6 +3036,26 @@ function FBHealBox_RefreshAllBars()
FBHealBox_RunHook("RefreshAllBars");
end
-- Nur die Plaketten auffrischen, deren Einheit in der Namensliste steht.
--
-- Der 0,2-Sekunden-Takt setzte frueher ein einziges Dirty-Flag: Lief
-- irgendwo ein HoT-Tick ab, wurde alles neu gezeichnet, im Vierzigerraid
-- also vierzig Zellen, obwohl sich bei genau einer Einheit etwas geaendert
-- hatte. Jetzt sammelt der Takt die betroffenen Namen ein und nur die werden
-- angefasst. Die Schleife selbst bleibt, sie kostet nur einen Tabellen-
-- zugriff je Plakette; teuer war immer die Aktualisierung dahinter.
function FBHealBox_RefreshUnitsByName(names)
if (not FBHealBox1) or (not FBHealBox1.ShieldBar) or (not names) then return; end
for p = 1, FBSlotCount do
local unit = FBPartyUnit[p];
if (FBUnitExists(unit)) then
local n = FBUnitName(unit);
if (n and names[n]) then FBHealBox_UpdateUnit(unit, FBPartyFrame[p]); end
end
end
FBHealBox_RunHook("RefreshNames", names);
end
-- Ist dieser Slot gerade anzuzeigen? (Begleiter nur mit ShowPets)
function FBSlotActive(p)
if (p == 1) then return true; end
@@ -3282,23 +3370,25 @@ function FBHealBox_SpellTimerFor(base, unitName, now, g, btnIndex)
return nil;
end
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
return tostring(math.ceil(e.expires - now)), FBTIMER_COLOR_HOT;
end
local sh = FBShields[unitName];
if (sh and sh.spell == base) then
if (sh.expires > now and (sh.max - sh.absorbed) > 0) then
return tostring(math.ceil(sh.expires - now)), FBTIMER_COLOR_SHIELD;
end
if (base == FBSHIELD_SPELL and sh.duration) then
local applied = sh.expires - sh.duration;
local left = applied + FBWEAKENED_SOUL_SEC - now;
if (left > 0) then return tostring(math.ceil(left)), FBTIMER_COLOR_WS; end
end
end
return nil;
local hots = FBHoTs[unitName];
local e = hots and hots[base];
-- vorlaeufige HoTs (am Buff erkannt, noch kein eigener Tick) zeigen nichts
if (e and e.expires > now and not e.provisional) then
return tostring(math.ceil(e.expires - now)), FBTIMER_COLOR_HOT;
end
-- Schildtimer nur fuer den eigenen, bestaetigten Schild
local sh = FBShields[unitName];
if (sh and sh.spell == base and sh.rankKnown and sh.expires > now and (sh.max - sh.absorbed) > 0) then
return tostring(math.ceil(sh.expires - now)), FBTIMER_COLOR_SHIELD;
end
-- Geschwaechte Seele aus eigener Tabelle. Bis 1.4.6 wurde sie aus dem
-- Schildeintrag berechnet, der beim Brechen des Schilds geloescht wird;
-- damit war sie praktisch nie zu sehen.
if (base == FBSHIELD_SPELL) then
local ws = FBWeakenedSoul[unitName];
if (ws and ws > now) then return tostring(math.ceil(ws - now)), FBTIMER_COLOR_WS; end
end
return nil;
end
function FBHealBox_FormatTimer(secs)
@@ -3321,16 +3411,24 @@ end
function FBHealBox_UpdateSpellTimers()
local now = GetTime();
local on = (HealBox.SpellTimers == 1);
-- Laeuft ueberhaupt ein eigener HoT oder Schild? Wenn nicht, gibt es auf
-- den Plaketten nichts anzuzeigen, und die Schleife braucht nur noch dort
-- hineinzuschauen, wo noch ein Timer wegzuraeumen ist. Spart bei Klassen
-- ohne HoT und zwischen den Kaempfen zehn Namensabfragen fuenfmal je
-- Sekunde. next() ist wahr, sobald die Tabelle einen Eintrag hat; leere
-- Untertabellen raeumt der Takt in FBPredict_OnUpdate weg.
local tracked = FBTestMode or (next(FBHoTs) ~= nil) or (next(FBShields) ~= nil)
or (next(FBWeakenedSoul) ~= nil);
for p = 1, FBSlotCount do
local f = FBPartyFrame[p];
local unit = FBPartyUnit[p];
if (f and FBPartyTable[p]) then
if (f and FBPartyTable[p] and (tracked or f.timersShown)) then
local shown = on and f:IsShown() and FBUnitExists(unit);
local name = shown and FBUnitName(unit);
local g = shown and FBTest_Ghost(unit);
-- 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]);
local active = shown and (g or FBHoTs[name] or FBShields[name] or FBWeakenedSoul[name]);
if (active) then
f.timersShown = true;
for i = 1, MaxButtonCount do
@@ -3403,6 +3501,19 @@ end
FBLOSFlags = {}; -- [Name] = Ablaufzeit der Markierung (Fallback-Weg)
-- Kandidat fuer eine Sichtlinien-Meldung: das Ziel des letzten Klicks auf
-- einen Button, bis dessen Cast entschieden ist. Die Sicht prueft der Server
-- beim Start und noch einmal am Ende eines Casts; laeuft der geklickte Zauber
-- an, gilt das Ziel deshalb bis Castende plus Spielraum. Abgeraeumt wird es,
-- sobald der Cast durchgeht (SPELLCAST_STOP), unterbrochen wird, ein anderer
-- Zauber anlaeuft oder eine andere Fehlermeldung den Klick beantwortet. Eine
-- spaetere Meldung (etwa nach einem Angriffszauber von der Aktionsleiste)
-- gehoert dann nicht mehr zu diesem Ziel.
FBLOSCandidate = nil;
FBLOSCandidateSpell = nil;
FBLOSCandidateUntil = 0;
FBLOS_ERROR_WINDOW = 1.0; -- Sek. Spielraum fuer die Antwort des Servers
function FBLOS_HasUnitXP()
return (UnitXP ~= nil);
end
@@ -3446,13 +3557,22 @@ function FBUnitLOSBlocked(unit)
return false;
end
-- Fehlermeldung "nicht in Sichtlinie": Ziel des letzten Heilversuchs markieren
-- Fehlermeldung "nicht in Sichtlinie": Ziel des gerade geklickten Buttons
-- markieren. Frueher fiel die Zuordnung auf das letzte Button-Ziel der
-- letzten zwei Sekunden, das freundliche Ziel oder den Spieler selbst
-- zurueck. Ein Angriffszauber ohne Sicht kurz nach einem Heilklick markierte
-- so den geheilten Mitspieler, ohne Heilklick die eigene Raidzelle.
function FBLOS_OnError(msg)
if (not msg) then return; end
-- Jede Fehlermeldung beantwortet den letzten Klick. War es eine andere
-- (Reichweite, Mana, "Another action is in progress"), kommt fuer ihn
-- keine Sichtlinien-Meldung mehr.
local name = FBLOSCandidate;
FBLOSCandidate = nil;
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
if (not name) or (GetTime() > FBLOSCandidateUntil) then return; end
if (name == UnitName("player")) then return; end
FBLOSFlags[name] = GetTime() + FBLOS_TIMEOUT;
FBHealBox_CheckLOSAll();
end
@@ -3563,6 +3683,7 @@ end
FBAPI_HealBonusFn = nil; -- gefundene Funktion oder nil
FBSpellCastCache = {}; -- [bookID] = Zauberzeit in Sekunden oder false
FBSpellCDCache = {}; -- [bookID] = Abklingzeit in Sekunden oder false
function FBHealBox_ProbeHealBonus()
FBAPI_HealBonusFn = nil;
@@ -3611,34 +3732,13 @@ function FBHealBox_HealingBonus()
return v;
end
-- Zauberzeit aus dem Tooltip. Sie steht rechts in der zweiten Zeile
-- ("2.5 sec cast"), Instants stehen dort als "Instant cast". 0 = Instant.
-- Zauberzeit aus dem Tooltip in Sekunden, 0 = Instant, nil = unlesbar.
-- Gelesen wird sie in FBPredict_TooltipText zusammen mit Reichweite und
-- Abklingzeit; hier wird nur der Zwischenspeicher abgefragt.
function FBHealBox_SpellCastSeconds(id)
if (not id) then return nil; end
local c = FBSpellCastCache[id];
if (c ~= nil) then return c or nil; end
-- Regelfall ist der Treffer oben: FBPredict_TooltipText fuellt den
-- Zwischenspeicher beim Auslesen der Zauberdaten mit. Hierher kommt nur,
-- wer noch nie durch die Tooltip-Auswertung gelaufen ist.
FBPredictTip:SetOwner(UIParent, "ANCHOR_NONE");
FBPredictTip:ClearLines();
FBPredictTip:SetSpell(id, BOOKTYPE_SPELL);
local secs = nil;
for i = 1, 6 do
for _, side in ipairs({ "Left", "Right" }) do
local fs = getglobal("FBHealBoxScanTipText"..side..i);
if (fs and fs:IsShown()) then
local t = fs:GetText();
if (t) then
local _, _, v = string.find(t, "([%d%.]+)%s+[Ss]ec%s+cast");
if (v) then secs = tonumber(v); end
if (not secs) and string.find(t, "[Ii]nstant") then secs = 0; end
end
end
end
end
FBSpellCastCache[id] = secs or false;
return secs;
if (FBSpellCastCache[id] == nil) then FBPredict_TooltipText(id); end
return FBSpellCastCache[id] or nil;
end
-- Anteil des +Heilung-Werts, der auf diesen Zauber entfaellt
@@ -3652,25 +3752,55 @@ function FBHealBox_HealBonusFor(bookID)
return bonus * (secs / 3.5);
end
-- Zauber, die unter Stufe 20 gelernt werden, bekommen in Vanilla nur einen
-- gekuerzten Anteil am +Heilung-Bonus: je Stufe unter 20 sind es 3,75 Prozent
-- weniger. Geringes Heilen Rang 3 (Stufe 10) erhaelt also 62,5 Prozent. Die
-- API verraet die Lernstufe eines Rangs nicht, deshalb stehen hier die Raenge
-- der Einzelzielheilungen, die unter Stufe 20 liegen (Stufe je Rang). Ohne
-- diesen Abschlag hielt Smart Healing kleine Raenge mit viel +Heilung fuer
-- deutlich staerker, als sie sind, und rangte zu tief ab.
FBRankLearnLevel = {
["Lesser Heal"] = { 1, 4, 10 },
["Heal"] = { 16 },
["Healing Touch"] = { 1, 8, 14 },
["Healing Wave"] = { 1, 6, 12, 18 },
["Holy Light"] = { 1, 6, 14 },
};
-- Faktor auf den Bonusanteil eines Rangs (1 = voller Anteil)
function FBHealBox_LowRankFactor(spellName, rank)
local levels = spellName and FBRankLearnLevel[spellName];
if (not levels) or (not rank) then return 1; end
local _, _, n = string.find(rank, "(%d+)");
local lvl = n and levels[tonumber(n)];
if (not lvl) or (lvl >= 20) then return 1; end
return 1 - (20 - lvl) * 0.0375;
end
-- Erwartete Sofortheilung eines Rangs: gelernter Wert, sonst Tooltip plus
-- anteiliger Ausruestungsbonus
-- anteiliger Ausruestungsbonus (bei Raengen unter Stufe 20 gekuerzt)
function FBPredict_ExpectedDirect(spellName, rank, info, bookID)
local learned = FBPredict_Remembered("direct", spellName, rank);
if (learned) then return learned; end
if (not info) or (not info.direct) then return nil; end
return info.direct + FBHealBox_HealBonusFor(bookID);
return info.direct + FBHealBox_HealBonusFor(bookID) * FBHealBox_LowRankFactor(spellName, rank);
end
-- Reichweite eines Zaubers aus dem Tooltip (Meter) oder nil
-- Reichweite eines Zaubers in Metern oder nil. Sie steht im Tooltip rechts
-- in Zeile 2 ("40 yd range") und wird in FBPredict_TooltipText gelesen.
-- Frueher wurde nur der linke Text durchsucht und die Reichweite nie
-- gefunden; der genaue Weg ueber UnitXP in FBHealBox_SpellInRange lief so nie.
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;
if (not id) then return nil; end
if (FBSpellRangeCache[id] == nil) then FBPredict_TooltipText(id); end
return FBSpellRangeCache[id] or nil;
end
-- Abklingzeit eines Zaubers in Sekunden oder nil (rechts in Zeile 3)
function FBHealBox_SpellCooldownSecs(id)
if (not id) then return nil; end
if (FBSpellCDCache[id] == nil) then FBPredict_TooltipText(id); end
return FBSpellCDCache[id] or nil;
end
-- Manapreis eines Zaubers aus dem Tooltip oder nil (Prozentkosten: nil)
@@ -4138,7 +4268,7 @@ function FBHealBoxCreateAddonOptionFrame()
classIcon.tex:SetAllPoints();
classIcon.tex:SetTexture(ClassIcon[FBClass]);
classIcon.text = classIcon:CreateFontString(nil, "OVERLAY", "GameFontNormal");
classIcon.text:SetText(strupper(FBClass));
classIcon.text:SetText(strupper(FBClassLocal or FBClass));
classIcon.text:SetPoint("TOP", classIcon, "BOTTOM", 0, -2);
classIcon.text:SetTextColor(1, 1, 0.2, 1);
@@ -4291,7 +4421,11 @@ function FBHealBoxCreateAddonOptionFrame()
getglobal(ScaleSlider:GetName() .. "High"):SetText(FBT("LARGE"));
ScaleSlider:SetScript("OnValueChanged", function()
HealBox.Scale = ScaleSlider:GetValue();
HealBoxScale(FBHealBox1, HealBox.Scale);
HealBoxScale(FBHealBox1, HealBox.Scale);
-- SetScale wertet die Ankerversaetze im neuen Massstab aus. Ohne
-- erneutes Verankern wanderte die Plakette beim Ziehen am Regler
-- und sprang erst nach dem naechsten Laden an ihren Platz zurueck.
if (HealBox.AttachMode ~= 1) then FBHealBox_RestorePosition(); end
FBUpdateScaleSliderText();
end);
@@ -5047,6 +5181,18 @@ FBPredictTickInterval = {
FBHoTs = {}; -- [Name] = { [Zauber] = {rank, perTick, interval, expires} }
FBShields = {}; -- [Name] = { spell, rank, max, absorbed, expires }
-- Geschwaechte Seele nach eigenem Machtwort: Schild, [Name] = Ablaufzeit.
-- Eigene Tabelle, weil der Schildeintrag genau dann verschwindet, wenn der
-- Schild bricht, also ab dem Moment, in dem die Seele ueberhaupt zaehlt.
FBWeakenedSoul = {};
-- HoTs, die am Buff erkannt, aber nie durch einen eigenen Tick bestaetigt
-- wurden: Sie stammen von einem anderen Heiler. [Name] = { [Zauber] = true },
-- bis der Buff verschwindet. Verhindert, dass sie bei jedem Aura-Ereignis
-- neu als vorlaeufig aufgenommen werden.
FBHoTForeign = {};
-- So lange nach dem ersten erwarteten Tick darf ein am Buff erkannter HoT
-- auf seinen ersten Tick "from your ..." warten, dann gilt er als fremd
FBPREDICT_HOT_CONFIRM_GRACE = 1.5;
FBBuffTimers = {}; -- [Name] = { [Zauber] = { expires } } Buffs mit Laufzeit (eigene Casts, eigene Buffs)
FBBuffPresent = {}; -- [Name] = { [Zauber] = true } Buff (Textur) ist auf der Einheit
FBPredictDirect = nil; -- laufender Direktcast { target, spell, rank, amount, finish }
@@ -5067,6 +5213,53 @@ for _, u in ipairs(FBPartyUnit) do FBPredictUnits[u] = 1; end
FBPredictTip = CreateFrame("GameTooltip", "FBHealBoxScanTip", nil, "GameTooltipTemplate");
FBPredictTip:SetOwner(UIParent, "ANCHOR_NONE");
-- Kopfzeilen eines Zaubertooltips im Client 1.12:
-- Zeile 1: Name links, Rang rechts
-- Zeile 2: Manapreis links, Reichweite rechts ("40 yd range")
-- Zeile 3: Zauberzeit links ("1.5 sec cast", "Instant cast"),
-- Abklingzeit rechts ("10 sec cooldown")
-- Gesucht wird in beiden Spalten der Kopfzeilen, damit leicht abweichende
-- Server nicht stoeren. Die Beschreibung darunter bleibt aussen vor.
FBTIP_HEAD_LINES = 5;
-- Zauberzeit in Sekunden, 0 = Instant, nil = nicht in diesem Text
function FBPredict_ParseCastTime(s)
if (not s) then return nil; end
local _, _, v = string.find(s, "([%d%.]+)%s+[Ss]ec%s+cast");
if (v) then return tonumber(v); end
if (string.find(s, "[Ii]nstant")) then return 0; end
return nil;
end
-- Reichweite in Metern oder nil
function FBPredict_ParseRange(s)
if (not s) then return nil; end
local _, _, yd = string.find(s, "(%d+)%s+[Yy]d");
if (not yd) then _, _, yd = string.find(s, "(%d+)%s+[Mm]eter"); end
if (yd) then return tonumber(yd); end
return nil;
end
-- Abklingzeit in Sekunden oder nil ("10 sec cooldown", "1.5 min cooldown")
function FBPredict_ParseCooldown(s)
if (not s) then return nil; end
local _, _, num, unit = string.find(s, "([%d%.]+)%s+(%a+)%s+[Cc]ooldown");
if (not num) then return nil; end
local n = tonumber(num);
if (not n) then return nil; end
unit = string.lower(unit);
if (string.find(unit, "^min")) then return n * 60; end
if (string.find(unit, "^h")) then return n * 3600; end
return n;
end
-- Liefert den Text der linken Spalte (fuer Betraege und Laufzeiten). Nebenbei
-- werden Zauberzeit, Reichweite und Abklingzeit aus den Kopfzeilen gelesen
-- und gemerkt, solange der Tooltip ohnehin steht.
-- Bis 1.4.6 wurde die Zauberzeit rechts und die Reichweite links gesucht,
-- also genau vertauscht. Die Reichweite fand sich nie, die Zauberzeit nur
-- fuer die Hoechstraenge ueber einen Rueckfallscan; alle anderen Raenge
-- rechneten den Ausruestungsbonus mit dem Ersatzwert 2,5 Sekunden.
function FBPredict_TooltipText(bookID)
if (not bookID) then return ""; end
FBPredictTip:SetOwner(UIParent, "ANCHOR_NONE");
@@ -5074,34 +5267,26 @@ function FBPredict_TooltipText(bookID)
FBPredictTip:SetSpell(bookID, BOOKTYPE_SPELL);
local txt = "";
local secs = nil;
local secs, range, cd = nil, nil, nil;
local i = 1;
while (i <= 30) do
local fs = getglobal("FBHealBoxScanTipTextLeft"..i);
if (not fs or not fs:IsShown()) then break; end
local line = fs:GetText();
if (line) then txt = txt.." "..line; end
-- Die Zauberzeit steht rechts ("2.5 sec cast", "Instant cast"). Sie
-- kommt bewusst nicht in txt: eine Zahl wie 2.5 wuerde die Muster
-- fuer Schilddauer und Heilbetrag durcheinanderbringen. Hier wird sie
-- nur nebenbei gelesen, solange der Tooltip ohnehin steht, und
-- gemerkt. Der Ausruestungsbonus braucht dann keinen eigenen Scan.
if (secs == nil) then
if (i <= FBTIP_HEAD_LINES) then
local rline = nil;
local rs = getglobal("FBHealBoxScanTipTextRight"..i);
if (rs and rs:IsShown()) then
local rline = rs:GetText();
if (rline) then
local _, _, v = string.find(rline, "([%d%.]+)%s+[Ss]ec%s+cast");
if (v) then secs = tonumber(v); end
if (secs == nil) and string.find(rline, "[Ii]nstant") then secs = 0; end
end
end
if (rs and rs:IsShown()) then rline = rs:GetText(); end
if (secs == nil) then secs = FBPredict_ParseCastTime(line) or FBPredict_ParseCastTime(rline); end
if (range == nil) then range = FBPredict_ParseRange(rline) or FBPredict_ParseRange(line); end
if (cd == nil) then cd = FBPredict_ParseCooldown(rline) or FBPredict_ParseCooldown(line); end
end
i = i + 1;
end
if (FBSpellCastCache[bookID] == nil) then
FBSpellCastCache[bookID] = secs or false;
end
if (FBSpellCastCache[bookID] == nil) then FBSpellCastCache[bookID] = secs or false; end
if (FBSpellRangeCache[bookID] == nil) then FBSpellRangeCache[bookID] = range or false; end
if (FBSpellCDCache[bookID] == nil) then FBSpellCDCache[bookID] = cd or false; end
return txt;
end
@@ -5429,42 +5614,60 @@ end
-- Nur dann darf der beobachtete Wert dauerhaft gemerkt werden, sonst
-- wuerde ein Rang-3-Cast von der Aktionsleiste die Schaetzung fuer den
-- Maximalrang verderben.
--
-- Ohne rankKnown (am Buff erkannt: Aktionsleiste, Makro oder ein anderer
-- Heiler) ist der Eintrag vorlaeufig. Er zaehlt weder in der Vorhersage noch
-- als Timer, bis der erste eigene Tick ("from your ...") ihn bestaetigt.
-- Kommt bis zum ersten erwarteten Tick plus FBPREDICT_HOT_CONFIRM_GRACE
-- keiner, verwirft FBPredict_OnUpdate ihn als fremd. Bis 1.4.6 lief jeder
-- erkannte HoT als eigener, mit dem eigenen Hoechstrang und voller Laufzeit.
function FBPredict_StartHoT(unitName, spellName, rank, bookID, rankKnown)
local info = FBPredict_GetSpellInfo(bookID, spellName);
if (not info) or (not info.hot) then return false; end
local now = GetTime();
local perTick = FBPredict_Remembered("tick", spellName, rank) or info.hot.perTick;
if (not FBHoTs[unitName]) then FBHoTs[unitName] = {}; end
FBHoTs[unitName][spellName] = {
local e = {
rank = rank,
rankKnown = rankKnown,
perTick = perTick,
interval = info.hot.interval,
expires = GetTime() + info.hot.duration,
expires = now + info.hot.duration,
};
if (not rankKnown) then
e.provisional = true;
e.provUntil = now + info.hot.interval + FBPREDICT_HOT_CONFIRM_GRACE;
end
FBHoTs[unitName][spellName] = e;
if (FBPredictDebug) then
local shown = spellName;
if (e.provisional) then shown = spellName.." (?)"; end
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[FBP]|r "..format(FBT("DBG_HOT"),
spellName, unitName, math.floor(perTick), info.hot.duration));
shown, unitName, math.floor(perTick), info.hot.duration));
end
-- Nur gesicherte eigene Casts funken. Ein per Aura entdeckter HoT
-- koennte auch von einem anderen Heiler stammen. Der erste eigene
-- Combatlog-Tick holt das unten nach.
if (rankKnown) then
FBHoTs[unitName][spellName].commSent = true;
e.commSent = true;
FBComm_SendHoT(spellName, unitName, info.hot.duration);
end
return true;
-- Ein vorlaeufiger Eintrag aendert an der Anzeige nichts
return (rankKnown and true) or false;
end
function FBPredict_StartShield(unitName, spellName, rank, bookID, rankKnown)
local info = FBPredict_GetSpellInfo(bookID, spellName);
if (not info) or (not info.shield) then return false; end
-- Gelernter Wert nur, wenn er plausibel ist (hoechstens das
-- FBPREDICT_ABSORB_MAX_FACTOR-fache des Tooltips); sonst Tooltip.
local amount = FBPredict_Remembered("absorb", spellName, rank);
-- Gelernter Wert nur bei einem eigenen, bestaetigten Cast und nur, wenn
-- er plausibel ist (hoechstens das FBPREDICT_ABSORB_MAX_FACTOR-fache des
-- Tooltips). Ein am Buff erkannter Schild kann von einem anderen Priester
-- stammen, dessen Ausruestung wir nicht kennen: dann der Tooltipwert.
local amount = rankKnown and FBPredict_Remembered("absorb", spellName, rank);
if (not amount) or (amount > info.shield.amount * FBPREDICT_ABSORB_MAX_FACTOR) then
amount = info.shield.amount;
end
@@ -5478,6 +5681,11 @@ function FBPredict_StartShield(unitName, spellName, rank, bookID, rankKnown)
absorbed = 0,
expires = GetTime() + info.shield.duration,
};
-- Geschwaechte Seele nur nach eigenem, bestaetigtem Machtwort: Schild.
-- Sie laeuft unabhaengig vom Schildeintrag weiter, wenn der Schild bricht.
if (rankKnown and spellName == FBSHIELD_SPELL) then
FBWeakenedSoul[unitName] = GetTime() + FBWEAKENED_SOUL_SEC;
end
if (FBPredictDebug) then
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[FBP]|r "..format(FBT("DBG_SHIELD"),
@@ -5503,8 +5711,11 @@ function FBGetHoTHeal(unitName)
local now = GetTime();
local sum = 0;
for _, e in pairs(t) do
local n = FBPredict_TicksLeft(e, now);
if (n > 0) then sum = sum + (n * e.perTick); end
-- vorlaeufige (noch nicht als eigen bestaetigte) HoTs zaehlen nicht
if (not e.provisional) then
local n = FBPredict_TicksLeft(e, now);
if (n > 0) then sum = sum + (n * e.perTick); end
end
end
return sum;
end
@@ -5547,6 +5758,10 @@ function FBPredict_NoteCast(castString, targetName)
FBPredictCastTime = GetTime();
local base, rank = FBPredict_SplitCast(castString);
-- Ziel fuer eine moegliche Sichtlinien-Meldung zu genau diesem Klick
FBLOSCandidate = targetName;
FBLOSCandidateSpell = base;
FBLOSCandidateUntil = FBPredictCastTime + FBLOS_ERROR_WINDOW;
if (not FBPredictWatch[base]) then return; end
FBPredictPending = {
@@ -5687,14 +5902,18 @@ function FBPredict_ScanUnit(unit)
if (w.hasHoT) then
local tracked = FBHoTs[name] and FBHoTs[name][spellName];
if (present and not tracked) then
-- z.B. von der Aktionsleiste: hoechster bekannter Rang,
-- der erste Combatlog-Tick korrigiert den Wert ohnehin
local foreign = FBHoTForeign[name] and FBHoTForeign[name][spellName];
if (present and not tracked and not foreign) then
-- z.B. von der Aktionsleiste, aber vielleicht auch von einem
-- anderen Heiler: vorlaeufig mit hoechstem bekanntem Rang, der
-- erste eigene Combatlog-Tick bestaetigt und korrigiert ihn
dirty = FBPredict_StartHoT(name, spellName, w.rank, w.bookID) or dirty;
elseif (tracked and not present) then
FBHoTs[name][spellName] = nil;
dirty = true;
if (not tracked.provisional) then dirty = true; end
end
-- Buff weg: die Fremdmarke gilt nur, solange er da ist
if (foreign and not present) then FBHoTForeign[name][spellName] = nil; end
end
if (w.hasShield) then
@@ -5790,16 +6009,42 @@ function FBPredict_ResolveVictim(event, msg)
return nil;
end
-- Eigener Tick ohne Eintrag: ein HoT von der Aktionsleiste oder aus einem
-- Makro, der nicht (mehr) verfolgt wird. Entweder kam sein erster Tick erst
-- nach der Frist, oder derselbe HoT eines anderen Heilers lag schon auf dem
-- Ziel und war als fremd gemerkt. "from your ..." beweist, dass er von uns
-- ist; er wird jetzt aufgenommen. Gerade ist mindestens ein Intervall
-- vorbei, um das die Laufzeit gekuerzt wird. true = Eintrag angelegt.
function FBPredict_AdoptOwnHoT(unitName, spellName)
local w = FBPredictWatch[spellName];
if (not w) or (not w.hasHoT) then return false; end
FBPredict_StartHoT(unitName, spellName, w.rank, w.bookID);
local e = FBHoTs[unitName] and FBHoTs[unitName][spellName];
if (not e) then return false; end
e.expires = e.expires - e.interval;
if (FBHoTForeign[unitName]) then FBHoTForeign[unitName][spellName] = nil; end
return true;
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
if (not t) or (not t[spellName]) then
if (not FBPredict_AdoptOwnHoT(unitName, spellName)) then return; end
t = FBHoTs[unitName];
end
local e = t[spellName];
-- "... from your Renew" beweist: der HoT ist von uns. Falls er ueber
-- die Aktionsleiste kam und noch nicht gefunkt wurde, jetzt nachholen.
-- "... from your Renew" beweist: der HoT ist von uns. Ein am Buff
-- erkannter, bisher vorlaeufiger Eintrag zaehlt ab jetzt in Vorhersage
-- und Timer. Falls er ueber die Aktionsleiste kam und noch nicht gefunkt
-- wurde, wird das jetzt nachgeholt.
if (e.provisional) then
e.provisional = nil;
e.provUntil = nil;
end
if (not e.commSent) then
e.commSent = true;
FBComm_SendHoT(spellName, unitName, e.expires - GetTime());
@@ -5989,19 +6234,36 @@ FBPredictFrame:SetScript("OnEvent", function()
FBLOS_OnError(arg1);
elseif (event == "SPELLCAST_START") then
-- Der geklickte Zauber laeuft an: Die Sicht kann noch am Castende
-- fehlen, sein Ziel bleibt bis dahin Kandidat. Ein anderer Zauber
-- gehoert nicht zu diesem Klick.
if (FBLOSCandidate) then
if (arg1 == FBLOSCandidateSpell) then
FBLOSCandidateUntil = GetTime() + (tonumber(arg2) or 0) / 1000 + FBLOS_ERROR_WINDOW;
else
FBLOSCandidate = nil;
end
end
FBPredict_CastStart(arg1, tonumber(arg2));
elseif (event == "SPELLCAST_STOP") then
-- Erfolgreich beendet: HealComm-Empfaenger lassen den Eintrag
-- selbst auslaufen, hier wird bewusst kein Healstop gefunkt.
FBLOSCandidate = nil; -- Cast ging durch, die Sicht war da
FBPredict_ConfirmBuffRefresh();
FBPredict_CastEnd();
elseif (event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED") then
-- Nach FAILED kann die Sichtlinien-Meldung noch folgen, nach einer
-- Unterbrechung (Bewegung, Unterbrechungszauber) nicht mehr
if (event == "SPELLCAST_INTERRUPTED") then FBLOSCandidate = nil; end
if (FBPredictDirect) then FBComm_SendHealStop(); end
FBPredict_CastEnd();
elseif (event == "SPELLCAST_DELAYED") then
if (FBLOSCandidate and arg1) then
FBLOSCandidateUntil = FBLOSCandidateUntil + (tonumber(arg1) or 0) / 1000;
end
if (FBPredictDirect and arg1) then
FBPredictDirect.finish = FBPredictDirect.finish + (tonumber(arg1) / 1000);
FBComm_SendHealDelay(arg1);
@@ -6019,6 +6281,8 @@ end);
FBNamesDirty = false;
FBBlizzPartyDirty = false;
FBDirtyNames = {}; -- im Takt betroffene Einheitennamen (wiederverwendet)
FBFullRefreshAccum = 0; -- Sekunden seit dem letzten vollen Durchlauf
FBBtnStatesDirty = nil;
FBBuffIconsDirty = true;
FBBuffIconsAccum = 0;
@@ -6069,39 +6333,79 @@ function FBPredict_OnUpdate(elapsed)
local now = GetTime();
local dirty = false;
local full = false;
for _, spells in pairs(FBHoTs) do
-- Betroffene Einheiten einsammeln statt pauschal alles neu zu zeichnen.
-- Die Tabelle wird wiederverwendet und nur geleert, damit je Durchlauf
-- keine neue entsteht.
local names = FBDirtyNames;
for k in pairs(names) do names[k] = nil; end
for uname, spells in pairs(FBHoTs) do
local anyLeft = false;
for spellName, e in pairs(spells) do
if (now >= e.expires) then
spells[spellName] = nil;
dirty = true;
if (not e.provisional) then
names[uname] = true;
dirty = true;
end
elseif (e.provisional) then
-- Am Buff erkannt, aber kein eigener Tick bis zur Frist: ein
-- HoT eines anderen Heilers. Wegwerfen und merken, damit er
-- bis zum Ende des Buffs nicht erneut aufgenommen wird. An
-- der Anzeige aendert das nichts, er zaehlte ja nie.
if (now >= e.provUntil) then
spells[spellName] = nil;
if (not FBHoTForeign[uname]) then FBHoTForeign[uname] = {}; end
FBHoTForeign[uname][spellName] = true;
else
anyLeft = true;
end
else
anyLeft = true;
local n = FBPredict_TicksLeft(e, now);
if (n ~= e.lastTicks) then
e.lastTicks = n;
names[uname] = true;
dirty = true;
end
end
end
-- leere Untertabelle wegraeumen: andernorts wird FBHoTs[name] als
-- "laeuft da ein HoT?" gelesen, und eine leere Tabelle ist wahr
if (not anyLeft) then FBHoTs[uname] = nil; end
end
for name, s in pairs(FBShields) do
if (now >= s.expires) then
FBShields[name] = nil;
names[name] = true;
dirty = true;
end
end
for _, casters in pairs(FBCommHeals) do
-- Abgelaufene Geschwaechte Seele wegraeumen (nur Buttontimer, kein Balken)
for name, untilT in pairs(FBWeakenedSoul) do
if (now >= untilT) then FBWeakenedSoul[name] = nil; end
end
for uname, casters in pairs(FBCommHeals) do
local anyLeft = false;
for caster, info in pairs(casters) do
if (now >= info.expires) then
casters[caster] = nil;
names[uname] = true;
dirty = true;
else
anyLeft = true;
end
end
if (not anyLeft) then FBCommHeals[uname] = nil; end
end
if (FBPredictDirect and now > FBPredictDirect.finish) then
if (FBPredictDirect.target) then names[FBPredictDirect.target] = true; end
FBPredictDirect = nil;
dirty = true;
end
@@ -6110,10 +6414,26 @@ function FBPredict_OnUpdate(elapsed)
FBPredictPending = nil;
end
-- Testmodus: die Geister atmen, im Takt der Vorhersage neu zeichnen
if (FBTestMode) then dirty = true; end
-- Testmodus: die Geister atmen, da muss alles mit
if (FBTestMode) then dirty = true; full = true; end
if (dirty) then FBHealBox_RefreshAllBars(); end
-- Sicherheitsnetz: hoechstens einmal je Sekunde doch der volle Durchlauf,
-- falls ein Name einmal zu keiner Plakette passt. Kostet gegenueber
-- frueher immer noch nur einen Bruchteil, weil es vorher mehrmals je
-- Sekunde passierte.
if (dirty) then
FBFullRefreshAccum = FBFullRefreshAccum + FBPREDICT_THROTTLE;
if (FBFullRefreshAccum >= 1.0) then
FBFullRefreshAccum = 0;
full = true;
end
end
if (full) then
FBHealBox_RefreshAllBars();
elseif (dirty) then
FBHealBox_RefreshUnitsByName(names);
end
end
FBPredict_InitPatterns();
@@ -6348,7 +6668,7 @@ SlashCmdList["FBHEALPREDICT"] = function(msg)
-- Optionsfenster und Diagnose waeren leer.
if (FBAddonSuppressed) then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "
..format(FBT("CLASS_BLOCKED"), FBClass or "?"));
..format(FBT("CLASS_BLOCKED"), FBClassLocal or FBClass or "?"));
DEFAULT_CHAT_FRAME:AddMessage("|cFFAAAAAA"..FBT("CLASS_BLOCKED_HINT").."|r");
return;
end
@@ -6470,6 +6790,14 @@ SlashCmdList["FBHEALPREDICT"] = function(msg)
parts = parts.." "..FBT("FBP_SHIELD").."="..info.shield.amount
.."/"..info.shield.duration.."s";
end
-- Kopfzeilen des Tooltips: Zauberzeit, Reichweite, Abklingzeit.
-- "?" heisst: nicht gefunden, dann rechnet das Addon mit Ersatzwerten.
local cast = FBHealBox_SpellCastSeconds(w.bookID);
local range = FBHealBox_SpellRangeYards(w.bookID);
local cd = FBHealBox_SpellCooldownSecs(w.bookID);
local head = " ["..((cast and (cast.."s")) or "?").." "..((range and (range.."yd")) or "?");
if (cd) then head = head.." cd "..cd.."s"; end
parts = parts..head.."]";
DEFAULT_CHAT_FRAME:AddMessage(" "..spellName.." ("..tostring(w.rank)..")"..parts);
end
end
@@ -6483,7 +6811,10 @@ SlashCmdList["FBHEALPREDICT"] = function(msg)
end
for name, spells in pairs(FBHoTs) do
for spellName, e in pairs(spells) do
DEFAULT_CHAT_FRAME:AddMessage(" HoT "..name.." / "..spellName..": "
-- "(?)" = am Buff erkannt, noch nicht durch einen eigenen Tick bestaetigt
local mark = "";
if (e.provisional) then mark = " (?)"; end
DEFAULT_CHAT_FRAME:AddMessage(" HoT "..name.." / "..spellName..mark..": "
..FBPredict_TicksLeft(e, now).." "..FBT("FBP_TICKSOF").." "
..math.floor(e.perTick));
end
+2 -2
View File
@@ -1,9 +1,9 @@
## Interface: 11200
## Title: Heal Box Vanilla |cFFFFFF00v1.4.5.3|r
## Title: Heal Box Vanilla |cFFFFFF00v1.4.6.1|r
## Notes: Heal/raid frame addon like Healium, with quick-cast buttons, heal prediction, mana ticker and more.
## Notes-deDE: Heal/Raidframe Addon aehnlich Healium, mit Schnellzugriff Buttons, Heal Prediction und Mana Ticker und mehr.
## Author: Dourd (Argent Dawn EU) | Vanilla-Port & Erweiterung 09/2026: Mquadrat
## Version: 1.4.5.3
## Version: 1.4.6.1
## SavedVariablesPerCharacter: HealBox
## OptionalDeps: SuperWoW
+51 -16
View File
@@ -290,6 +290,7 @@ FBRaidTestMode = 0; -- 0 | 20 | 40
FBRaidTestGhosts = {}; -- [i] = Geist-Datensatz (Testmodus)
FBRaidTickAccum = 0;
FBRaidDragging = false;
FBRaidAppliedScale = nil; -- zuletzt gesetzte Skalierung (FBRaid_LayoutAll)
function FBRaid_Cfg()
if (not HealBox.Raid) then FBRaid_ApplyDefaults(); end
@@ -664,7 +665,12 @@ function FBRaid_CreateCell(g, pos)
if (FBDragSpell) then
local side = "L";
if (arg1 == "RightButton" or IsShiftKeyDown()) then side = "R"; end
if (FBHealBox_DropSpell(i, side)) then return; end
-- Nicht die Schleifenvariable i verwenden: Lua 5.0 (Client
-- 1.12) teilt sie zwischen allen hier erzeugten Closures, alle
-- vier Buttons saehen ihren Endwert und legten den Zauber
-- auf Button 4. b ist dagegen je Durchlauf eine neue lokale
-- Variable.
if (FBHealBox_DropSpell(b.btnIndex, side)) then return; end
end
if (FBRaidTestMode > 0) then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..FBT("RAID_TEST_CLICK"));
@@ -827,7 +833,16 @@ function FBRaid_LayoutAll()
local titleH = 0;
if (cfg.ShowTitle == 1) then titleH = FBRAID_TITLE_H + 2; FBRaidFrame.Title:Show(); else FBRaidFrame.Title:Hide(); end
FBRaidFrame:SetScale(cfg.Scale or 1);
-- SetScale wertet die Ankerversaetze im neuen Massstab aus. Ohne erneutes
-- Verankern wanderte das Raster beim Ziehen am Regler und sprang erst
-- beim naechsten Rosterwechsel zurueck. Nur bei echter Aenderung, und
-- nie waehrend das Raster gerade gezogen wird.
local scale = cfg.Scale or 1;
if (FBRaidAppliedScale ~= scale) then
FBRaidAppliedScale = scale;
FBRaidFrame:SetScale(scale);
if (not FBRaidDragging) then FBRaid_RestorePosition(); end
end
local perRow = cfg.GroupsPerRow;
if (perRow < 1) then perRow = 1; end
@@ -1093,21 +1108,25 @@ function FBRaid_UpdateSpellTimers(now)
for pos = 1, FBRAID_PER_GROUP do
local c = FBRaidCells[g][pos];
if (c) then
-- 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.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);
local shown = on and c.unit and c:IsShown() and (c.ghost or FBHoTs[c.name] or FBShields[c.name]
or (FBWeakenedSoul and FBWeakenedSoul[c.name]));
-- Nichts anzuzeigen und nichts mehr wegzuraeumen: Zelle
-- ueberspringen. Vorher lief die Buttonschleife auch dann
-- durch, im Vierzigerraid 160 Leeraufrufe fuenfmal je
-- Sekunde. Der Kern macht es bei den Plaketten genauso.
if (shown or c.timersShown) then
c.timersShown = (shown and true) or false;
for i = 1, FBRAID_MAX_BUTTONS do
local b = c.buttons[i];
if (shown and b:IsShown()) then
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
FBHealBox_SetButtonTimer(b, nil);
end
FBHealBox_SetButtonTimer(b, text, color);
else
FBHealBox_SetButtonTimer(b, nil);
end
end
end
@@ -1583,6 +1602,22 @@ FBHealBox_RegisterHook("ActiveToggle", function()
FBRaid_UpdateVisibility();
return true;
end);
-- Nur die Zellen anfassen, deren Einheit sich geaendert hat. Der Kern
-- schickt die Namensliste mit; eine Zelle, die nicht drinsteht, kostet
-- einen Tabellenzugriff statt einer vollen Aktualisierung.
FBHealBox_RegisterHook("RefreshNames", function(names)
if (not FBRaid_IsActive()) or (not names) then return true; end
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.name and names[c.name]) then FBRaid_UpdateCell(c); end
end
end
end
return true;
end);
FBHealBox_RegisterHook("RefreshAllBars", function()
FBRaid_RefreshAll();
return true;
+8 -5
View File
@@ -1,6 +1,6 @@
# Heal Box Vanilla
ADDON DOCUMENTATION · VERSION 1.4.5.3 · World of Warcraft CLIENT 1.12.1
ADDON DOCUMENTATION · VERSION 1.4.6 · World of Warcraft CLIENT 1.12.1
Party, pet and self heal display with quick-cast buttons for healers. One name plate with a health bar per group slot, plus one for every pet in the group directly below its owner, and next to it up to ten freely assignable spell buttons. A thin mana bar sits inside the health bar for everyone who actually uses mana. On top of that a complete heal prediction (direct heals, remaining HoT ticks and absorb shields) that corrects itself from the combat log and shares its numbers with other healers in the HealComm format. The interface is available in **English and German**, switchable in the options window.
@@ -584,6 +584,7 @@ Every knob is a global at the top of its own section and can be changed without
|`FBHealBox_ApplyBlizzParty()` · `FBHealBox_HideBlizzPartyActive()`|Hides or restores Blizzard's party frames, including the lock against the attach mode|
|`FBHealBox_SetPowerColor(bar, cache, ptype)`|Colours a resource bar for mana, rage, focus or energy|
|`FBHealBox_ApplyBarBG(f)` · `FBHealBox_ApplyBarBGAll()`|Sets the background behind a plate's bars from the `BarBG` option|
|`FBHealBox_RefreshUnitsByName(names)`|Redraws only the plates whose unit is in the name set; the raid module gets the same set through the `RefreshNames` hook|
|`FBPredict_ScanPlayerBuffTimes()`|Reads all of your own buff timers once per frame into a texture-to-seconds map|
|`FBHealBox_ApiFailed()`|Puts range, distance and line of sight back to the protected single call after a sweep threw|
|`FBHealBox_UpperTex(tex)`|Upper-cased texture path, remembered, so `strupper` does not allocate per call|
@@ -744,6 +745,7 @@ Entries that do not exist do no harm: if the spellbook scan does not find them,
* heal prediction for direct heals, remaining HoT ticks and absorb shields, self-correcting from the combat log
* HealComm sync with Puppeteer, pfUI, Luna and others, without any Ace libraries
* English, German, Spanish, French and Italian localization, switchable in game
* 1.4.6: performance pass without functional change (only changed units are redrawn via the `RefreshNames` hook, raid spell timers skip idle cells, spell timers leave early when nothing is running); see CHANGELOG
* 1.4.5.3: adjustable background behind the plate bars, so the game world no longer shows through where health is missing; see CHANGELOG
* 1.4.5.2: a running HoT no longer counts as incoming healing when Smart Healing picks a rank; see CHANGELOG
* 1.4.5.1: mana ticker spark no longer stutters (movement threshold and redraw step), and the global cooldown no longer darkens every button but is shown as a sweep instead (own threshold `FBCD_SHOW_MIN`); see CHANGELOG
@@ -758,14 +760,14 @@ Entries that do not exist do no harm: if the spellbook scan does not find them,
---
Heal Box Vanilla v1.4.5.3 · original by Dourd, UI Overhauled · ported to Vanilla and extended 09/2026 by Mquadrat
Heal Box Vanilla v1.4.6 · original by Dourd, UI Overhauled · ported to Vanilla and extended 09/2026 by Mquadrat
_______________________________________________________________________
GERMAN
# Heal Box Vanilla
ADDON DOKUMENTATION · VERSION 1.4.5.3 · World of Warcraft CLIENT 1.12.1
ADDON DOKUMENTATION · VERSION 1.4.6 · World of Warcraft CLIENT 1.12.1
Party-, Begleiter- und Selbst-Heilanzeige mit Schnellzugriff-Buttons für Heiler. Für jeden Gruppenplatz eine Namensplakette mit Lebensbalken, dazu eine für jeden Begleiter in der Gruppe direkt unter seinem Besitzer, daneben bis zu zehn frei belegbare Zauber-Buttons. Ein schmaler Manabalken liegt im Lebensbalken, bei allen, die tatsächlich Mana nutzen. Dazu eine vollständige Heilvorhersage (Direktheilung, HoT-Restticks und Absorb-Schilde), die sich über den Combatlog selbst korrigiert und ihre Werte im HealComm-Format mit anderen Heilern teilt. Die Oberfläche gibt es auf **Deutsch und Englisch**, umschaltbar im Optionsfenster.
@@ -1336,6 +1338,7 @@ Alle Stellschrauben stehen als Globals oben in ihrem jeweiligen Abschnitt und la
|`FBHealBox_ApplyBlizzParty()` · `FBHealBox_HideBlizzPartyActive()`|Versteckt Blizzards Gruppenfenster oder gibt sie zurück, samt Sperre gegen den Anheftmodus|
|`FBHealBox_SetPowerColor(bar, cache, ptype)`|Färbt einen Ressourcenbalken für Mana, Wut, Fokus oder Energie|
|`FBHealBox_ApplyBarBG(f)` · `FBHealBox_ApplyBarBGAll()`|Setzt den Hintergrund hinter den Balken einer Plakette nach der Option `BarBG`|
|`FBHealBox_RefreshUnitsByName(names)`|Zeichnet nur die Plaketten neu, deren Einheit in der Namensliste steht; das Raidmodul bekommt dieselbe Liste über den Hook `RefreshNames`|
|`FBPredict_ScanPlayerBuffTimes()`|Liest alle eigenen Bufflaufzeiten einmal je Frame in eine Tabelle Textur nach Sekunden|
|`FBHealBox_ApiFailed()`|Stellt Reichweite, Abstand und Sichtlinie auf den geschützten Einzelaufruf zurück, wenn ein Durchlauf geworfen hat|
|`FBHealBox_UpperTex(tex)`|Groß geschriebener Texturpfad, gemerkt, damit `strupper` nicht je Aufruf zuteilt|
@@ -1485,6 +1488,7 @@ Nicht vorhandene Einträge stören nicht: Findet der Zauberbuch-Scan sie nicht,
* Heilvorhersage für Direktheilung, HoT-Restticks und Absorb-Schilde, selbstkorrigierend über den Combatlog
* HealComm-Sync mit Puppeteer, pfUI, Luna und Co., ohne Ace-Bibliotheken
* Lokalisierung Deutsch, Englisch, Spanisch, Französisch und Italienisch, im laufenden Spiel umschaltbar
* 1.4.6: Leistungsdurchgang ohne Funktionsänderung (nur geänderte Einheiten werden neu gezeichnet, über den Hook `RefreshNames`, Raid-Zaubertimer überspringen stille Zellen, Zaubertimer steigen früher aus); siehe CHANGELOG
* 1.4.5.3: Einstellbarer Hintergrund hinter den Balken der Plaketten, damit die Spielwelt nicht mehr durch fehlendes Leben scheint; siehe CHANGELOG
* 1.4.5.2: Ein laufender HoT zählt bei der Rangwahl von Smart Healing nicht mehr als anfliegende Heilung; siehe CHANGELOG
* 1.4.5.1: Ruckeln des Manafunkens behoben (Bewegungsschwelle und Zeichentakt), der globale Cooldown dunkelt nicht mehr alle Buttons ab und läuft stattdessen als Uhr mit (eigene Schwelle `FBCD_SHOW_MIN`); siehe CHANGELOG
@@ -1499,7 +1503,7 @@ Nicht vorhandene Einträge stören nicht: Findet der Zauberbuch-Scan sie nicht,
---
Heal Box Vanilla v1.4.5.3 · Original von Dourd, UI Overhauled · Vanilla-Portierung und Erweiterung 09/2026 von Mquadrat
Heal Box Vanilla v1.4.6 · Original von Dourd, UI Overhauled · Vanilla-Portierung und Erweiterung 09/2026 von Mquadrat
---
@@ -1510,4 +1514,3 @@ Heal Box Vanilla v1.4.5.3 · Original von Dourd, UI Overhauled · Vanilla-Portie
**Deutsch:** World of Warcraft Vanilla 1.12.1 Heiler-Addon, Classic WoW Heil-Addon, Heilfenster mit Klick-Buttons, Healium-Alternative für Vanilla, HealComm-kompatibel, Heilvorhersage und eingehende Heilung, Absorb-Schild-Anzeige, HoT-Timer, Machtwort: Schild und Geschwächte Seele, Buff-Timer als Uhr-Icons, Seelenstärke nachbuffen, Dispel-Anzeige, Gruppen- und Begleiterfenster, kompakte Raidframes für 20er und 40er Schlachtzüge, Raid-Raster für Heiler, Mana-Ticker und Fünf-Sekunden-Regel, Regenerations-Tick-Balken, automatisches Abrangen, Smart Healing, Smart Damage, Angriffsziel-Markierung, Sichtlinien-Anzeige, Reichweiten-Fading, Klassenfarben, SuperWoW, UnitXP, MobHealth3 und MobInfo-2, läuft auf Turtle WoW und anderen 1.12-Privatservern, Priester Druide Paladin Schamane Heiler-UI, Lua 5.0 Addon, Deutsch Englisch Spanisch Französisch Italienisch.
#WoWVanilla #Vanilla112 #WoW1121 #ClassicWoW #TurtleWoW #WoWAddon #VanillaAddon #HealerAddon #HealingAddon #Healium #HealComm #HealPrediction #RaidFrames #PartyFrames #UnitFrames #ClickCasting #ManaTicker #FiveSecondRule #Downranking #SmartHealing #SmartDamage #BuffTimer #HoTTracker #DispelTracker #Priest #Druid #Paladin #Shaman #SuperWoW #UnitXP #MobHealth #Lua50 #HealBoxVanilla