1 Commits
Author SHA1 Message Date
Mquadrat fac63b2d19 Add files via upload 2026-10-07 22:48:10 +02:00
3 changed files with 394 additions and 116 deletions
+373 -111
View File
@@ -47,6 +47,12 @@
-- * 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.
@@ -80,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.
@@ -162,7 +184,7 @@ HealBox = {
-- feuert ADDON_LOADED fuer uns.
FBADDON_NAME = "Heal Box Vanilla";
FBADDON_FOLDER = "FBHealBox";
HealBoxVersion = "|cFFFFFF00v1.4.6|r";
HealBoxVersion = "|cFFFFFF00v1.4.6.1|r";
-- ==========================================================================
-- [ Lokalisierung / Localization ]
@@ -209,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"
@@ -310,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.",
@@ -361,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"
@@ -462,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.",
@@ -538,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"
@@ -633,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.",
@@ -681,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"
@@ -776,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.",
@@ -824,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"
@@ -919,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.",
@@ -1429,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
@@ -1466,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;
@@ -1672,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
@@ -2715,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.
@@ -2745,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);
@@ -2777,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
@@ -3305,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)
@@ -3350,7 +3417,8 @@ function FBHealBox_UpdateSpellTimers()
-- 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);
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];
@@ -3360,7 +3428,7 @@ function FBHealBox_UpdateSpellTimers()
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
@@ -3433,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
@@ -3476,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
@@ -3593,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;
@@ -3641,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
@@ -3682,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)
@@ -4168,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);
@@ -4321,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);
@@ -5077,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 }
@@ -5097,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");
@@ -5104,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
@@ -5459,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
@@ -5508,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"),
@@ -5533,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
@@ -5577,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 = {
@@ -5717,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
@@ -5820,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());
@@ -6019,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);
@@ -6114,8 +6346,22 @@ function FBPredict_OnUpdate(elapsed)
for spellName, e in pairs(spells) do
if (now >= e.expires) then
spells[spellName] = nil;
names[uname] = true;
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);
@@ -6139,6 +6385,11 @@ function FBPredict_OnUpdate(elapsed)
end
end
-- 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
@@ -6417,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
@@ -6539,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
@@ -6552,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.6|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.6
## Version: 1.4.6.1
## SavedVariablesPerCharacter: HealBox
## OptionalDeps: SuperWoW
+19 -3
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,7 +1108,8 @@ 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() and (c.ghost or FBHoTs[c.name] or FBShields[c.name]);
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