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 -- * v1.4.6: Leistungsdurchgang ohne Funktionsaenderung: nur geaenderte
-- Einheiten werden neu gezeichnet (FBHealBox_RefreshUnitsByName, Hook -- Einheiten werden neu gezeichnet (FBHealBox_RefreshUnitsByName, Hook
-- "RefreshNames"), Zaubertimer steigen frueher aus. -- "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 -- Ehre wem Ehre gebuehrt: Aufbau, Namensplaketten und Grundidee stammen
-- aus dem Original. -- aus dem Original.
@@ -80,6 +86,22 @@ do
if (eng) then FBClassToken = strupper(eng); end if (eng) then FBClassToken = strupper(eng); end
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 -- Rueckfall, wenn der Client kein englisches Token liefert: angezeigter
-- Klassenname. Bewusst in mehreren Sprachen, damit die Sperre auch auf -- Klassenname. Bewusst in mehreren Sprachen, damit die Sperre auch auf
-- lokalisierten Clients greift. -- lokalisierten Clients greift.
@@ -162,7 +184,7 @@ HealBox = {
-- feuert ADDON_LOADED fuer uns. -- feuert ADDON_LOADED fuer uns.
FBADDON_NAME = "Heal Box Vanilla"; FBADDON_NAME = "Heal Box Vanilla";
FBADDON_FOLDER = "FBHealBox"; FBADDON_FOLDER = "FBHealBox";
HealBoxVersion = "|cFFFFFF00v1.4.6|r"; HealBoxVersion = "|cFFFFFF00v1.4.6.1|r";
-- ========================================================================== -- ==========================================================================
-- [ Lokalisierung / Localization ] -- [ Lokalisierung / Localization ]
@@ -209,7 +231,7 @@ FBLocale["enUS"] = {
SMARTRANK = "Smart Healing", 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" 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" .. "|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" .. "• A HoT on the target is not counted as incoming\n"
.. "• Always casts assigned rank below 30 % health\n" .. "• Always casts assigned rank below 30 % health\n"
.. "• Never heals more than the assigned rank\n" .. "• Never heals more than the assigned rank\n"
@@ -310,6 +332,7 @@ FBLocale["enUS"] = {
DBG_HEAL = "Heal %s = %d (estimate now %d)", DBG_HEAL = "Heal %s = %d (estimate now %d)",
DBG_ABSORB = "Absorb %d on %s (%d left)", DBG_ABSORB = "Absorb %d on %s (%d left)",
DBG_SMART_HOT = "Smart Healing skipped: %s is a heal over time", 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", 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_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.", 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 = "Smart Healing",
SMARTRANK_TIP = "Wirkt automatisch den niedrigsten Zauberrang, der das fehlende Leben (abzgl. eingehender Heilung) plus Sicherheitsaufschlag deckt.\n\n" SMARTRANK_TIP = "Wirkt automatisch den niedrigsten Zauberrang, der das fehlende Leben (abzgl. eingehender Heilung) plus Sicherheitsaufschlag deckt.\n\n"
.. "|cFFFFD100Regeln:|r\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" .. "• Laufender HoT zaehlt nicht als anfliegende Heilung\n"
.. "• Unter 30 % Leben immer der belegte Rang\n" .. "• Unter 30 % Leben immer der belegte Rang\n"
.. "• Nie mehr Heilung als 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_HEAL = "Heilung %s = %d (Schaetzung jetzt %d)",
DBG_ABSORB = "Absorb %d auf %s (Rest %d)", DBG_ABSORB = "Absorb %d auf %s (Rest %d)",
DBG_SMART_HOT = "Smart Healing uebersprungen: %s ist Heilung ueber Zeit", 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", 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_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.", 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 = "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" 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" .. "|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" .. "• Un HoT activo no cuenta como curación entrante\n"
.. "• Siempre el rango asignado bajo 30 % de vida\n" .. "• Siempre el rango asignado bajo 30 % de vida\n"
.. "• Nunca cura más que el rango asignado\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_HEAL = "Curación %s = %d (estimación ahora %d)",
DBG_ABSORB = "Absorción %d en %s (quedan %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_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", 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_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.", 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 = "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" 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" .. "|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" .. "• Un HoT actif ne compte pas comme soin en cours\n"
.. "• Rang assigné conservé sous 30 % de vie\n" .. "• Rang assigné conservé sous 30 % de vie\n"
.. "• Ne soigne jamais plus que le rang assigné\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_HEAL = "Soin %s = %d (estimation maintenant %d)",
DBG_ABSORB = "Absorption %d sur %s (reste %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_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", 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_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.", 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 = "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" 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" .. "|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" .. "• Un HoT attivo non conta come cura in arrivo\n"
.. "• Sotto il 30 % di salute lancia sempre il rango assegnato\n" .. "• Sotto il 30 % di salute lancia sempre il rango assegnato\n"
.. "• Mai una cura superiore al 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_HEAL = "Cura %s = %d (stima ora %d)",
DBG_ABSORB = "Assorbimento %d su %s (restano %d)", DBG_ABSORB = "Assorbimento %d su %s (restano %d)",
DBG_SMART_HOT = "Smart Healing saltato: %s è una cura nel tempo", 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", 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_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.", 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("|cFFFFFF00"..FBADDON_NAME.."|r "..HealBoxVersion.." : |cFF00FF00"..FBT("LOADED")..swowTag);
DEFAULT_CHAT_FRAME:AddMessage(FBT("CREDITS")); DEFAULT_CHAT_FRAME:AddMessage(FBT("CREDITS"));
if (FBClassBlocked) then 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 end
FBHealBox_RunHook("Loaded"); -- Module melden sich im Chat FBHealBox_RunHook("Loaded"); -- Module melden sich im Chat
end end
@@ -1466,7 +1493,7 @@ function FBHealBox_ApplyClassGate()
if (not FBGateAnnounced) then if (not FBGateAnnounced) then
FBGateAnnounced = true; FBGateAnnounced = true;
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME.."|r "..HealBoxVersion..": |cFFFF8000" 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"); DEFAULT_CHAT_FRAME:AddMessage("|cFFAAAAAA"..FBT("CLASS_BLOCKED_HINT").."|r");
end end
return false; return false;
@@ -1672,10 +1699,18 @@ function FBLoadSpellData()
i = i + 1; i = i + 1;
end 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? -- Tooltips auswerten: welche dieser Zauber sind HoTs bzw. Absorb-Schilde?
FBPredict_BuildWatch(); FBPredict_BuildWatch();
FBSpellNameCache = {};
FBSpellCastCache = {};
FBHealBox_InvalidateRangeSpell(); FBHealBox_InvalidateRangeSpell();
FBHealBox_ProbeAPIs(); FBHealBox_ProbeAPIs();
-- Doppelt gezaehlte Absorb-Lernwerte (Versionen vor 1.4.2) verwerfen -- Doppelt gezaehlte Absorb-Lernwerte (Versionen vor 1.4.2) verwerfen
@@ -2715,6 +2750,28 @@ for _, chain in ipairs(FBHealChains) do
end end
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 -- Erwartete Heilung eines Rangs, oder nil, wenn der Zauber fuer Smart
-- Healing nicht in Frage kommt: kein Heilbetrag, Schild, HoT-Anteil, Buff -- Healing nicht in Frage kommt: kein Heilbetrag, Schild, HoT-Anteil, Buff
-- oder kein Heiltext im Tooltip. Gelernter Wert schlaegt den Tooltip. -- oder kein Heiltext im Tooltip. Gelernter Wert schlaegt den Tooltip.
@@ -2745,6 +2802,13 @@ function FBHealBox_SmartRank(castString, unit)
end end
return castString; return castString;
end 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 if (not FBUnitExists(unit)) or FBTest_Ghost(unit) then return castString; end
local hp, hpMax = FBUnitHealth(unit); local hp, hpMax = FBUnitHealth(unit);
@@ -2777,7 +2841,8 @@ function FBHealBox_SmartRank(castString, unit)
local bestSD, bestName, bestAmount = assignedSD, base, cap; local bestSD, bestName, bestAmount = assignedSD, base, cap;
for _, spellName in ipairs(FBHealBox_HealFamily(base)) do for _, spellName in ipairs(FBHealBox_HealFamily(base)) do
local list = FBPlayerSpells[spellName]; 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 for _, sd in ipairs(list) do
local amount = FBHealBox_DirectAmount(spellName, sd); local amount = FBHealBox_DirectAmount(spellName, sd);
if (amount) and (amount >= need) and (amount < bestAmount) then if (amount) and (amount >= need) and (amount < bestAmount) then
@@ -3305,23 +3370,25 @@ function FBHealBox_SpellTimerFor(base, unitName, now, g, btnIndex)
return nil; return nil;
end end
if (not base) or (not unitName) then return nil; end if (not base) or (not unitName) then return nil; end
local hots = FBHoTs[unitName]; local hots = FBHoTs[unitName];
local e = hots and hots[base]; local e = hots and hots[base];
if (e and e.expires > now) then -- vorlaeufige HoTs (am Buff erkannt, noch kein eigener Tick) zeigen nichts
return tostring(math.ceil(e.expires - now)), FBTIMER_COLOR_HOT; if (e and e.expires > now and not e.provisional) then
end return tostring(math.ceil(e.expires - now)), FBTIMER_COLOR_HOT;
local sh = FBShields[unitName]; end
if (sh and sh.spell == base) then -- Schildtimer nur fuer den eigenen, bestaetigten Schild
if (sh.expires > now and (sh.max - sh.absorbed) > 0) then local sh = FBShields[unitName];
return tostring(math.ceil(sh.expires - now)), FBTIMER_COLOR_SHIELD; if (sh and sh.spell == base and sh.rankKnown and sh.expires > now and (sh.max - sh.absorbed) > 0) then
end return tostring(math.ceil(sh.expires - now)), FBTIMER_COLOR_SHIELD;
if (base == FBSHIELD_SPELL and sh.duration) then end
local applied = sh.expires - sh.duration; -- Geschwaechte Seele aus eigener Tabelle. Bis 1.4.6 wurde sie aus dem
local left = applied + FBWEAKENED_SOUL_SEC - now; -- Schildeintrag berechnet, der beim Brechen des Schilds geloescht wird;
if (left > 0) then return tostring(math.ceil(left)), FBTIMER_COLOR_WS; end -- damit war sie praktisch nie zu sehen.
end if (base == FBSHIELD_SPELL) then
end local ws = FBWeakenedSoul[unitName];
return nil; if (ws and ws > now) then return tostring(math.ceil(ws - now)), FBTIMER_COLOR_WS; end
end
return nil;
end end
function FBHealBox_FormatTimer(secs) function FBHealBox_FormatTimer(secs)
@@ -3350,7 +3417,8 @@ function FBHealBox_UpdateSpellTimers()
-- ohne HoT und zwischen den Kaempfen zehn Namensabfragen fuenfmal je -- ohne HoT und zwischen den Kaempfen zehn Namensabfragen fuenfmal je
-- Sekunde. next() ist wahr, sobald die Tabelle einen Eintrag hat; leere -- Sekunde. next() ist wahr, sobald die Tabelle einen Eintrag hat; leere
-- Untertabellen raeumt der Takt in FBPredict_OnUpdate weg. -- 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 for p = 1, FBSlotCount do
local f = FBPartyFrame[p]; local f = FBPartyFrame[p];
local unit = FBPartyUnit[p]; local unit = FBPartyUnit[p];
@@ -3360,7 +3428,7 @@ function FBHealBox_UpdateSpellTimers()
local g = shown and FBTest_Ghost(unit); local g = shown and FBTest_Ghost(unit);
-- Ohne eigenen HoT oder Schild auf dieser Einheit gibt es nichts -- Ohne eigenen HoT oder Schild auf dieser Einheit gibt es nichts
-- anzuzeigen: dann nur einmal alle Timer ausblenden und weiter. -- 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 if (active) then
f.timersShown = true; f.timersShown = true;
for i = 1, MaxButtonCount do for i = 1, MaxButtonCount do
@@ -3433,6 +3501,19 @@ end
FBLOSFlags = {}; -- [Name] = Ablaufzeit der Markierung (Fallback-Weg) 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() function FBLOS_HasUnitXP()
return (UnitXP ~= nil); return (UnitXP ~= nil);
end end
@@ -3476,13 +3557,22 @@ function FBUnitLOSBlocked(unit)
return false; return false;
end 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) function FBLOS_OnError(msg)
if (not msg) then return; end 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"; local losText = SPELL_FAILED_LINE_OF_SIGHT or "Target not in line of sight";
if (msg ~= losText) then return; end if (msg ~= losText) then return; end
local name = FBPredict_ResolveTarget(); if (not name) or (GetTime() > FBLOSCandidateUntil) then return; end
if (not name) then return; end if (name == UnitName("player")) then return; end
FBLOSFlags[name] = GetTime() + FBLOS_TIMEOUT; FBLOSFlags[name] = GetTime() + FBLOS_TIMEOUT;
FBHealBox_CheckLOSAll(); FBHealBox_CheckLOSAll();
end end
@@ -3593,6 +3683,7 @@ end
FBAPI_HealBonusFn = nil; -- gefundene Funktion oder nil FBAPI_HealBonusFn = nil; -- gefundene Funktion oder nil
FBSpellCastCache = {}; -- [bookID] = Zauberzeit in Sekunden oder false FBSpellCastCache = {}; -- [bookID] = Zauberzeit in Sekunden oder false
FBSpellCDCache = {}; -- [bookID] = Abklingzeit in Sekunden oder false
function FBHealBox_ProbeHealBonus() function FBHealBox_ProbeHealBonus()
FBAPI_HealBonusFn = nil; FBAPI_HealBonusFn = nil;
@@ -3641,34 +3732,13 @@ function FBHealBox_HealingBonus()
return v; return v;
end end
-- Zauberzeit aus dem Tooltip. Sie steht rechts in der zweiten Zeile -- Zauberzeit aus dem Tooltip in Sekunden, 0 = Instant, nil = unlesbar.
-- ("2.5 sec cast"), Instants stehen dort als "Instant cast". 0 = Instant. -- Gelesen wird sie in FBPredict_TooltipText zusammen mit Reichweite und
-- Abklingzeit; hier wird nur der Zwischenspeicher abgefragt.
function FBHealBox_SpellCastSeconds(id) function FBHealBox_SpellCastSeconds(id)
if (not id) then return nil; end if (not id) then return nil; end
local c = FBSpellCastCache[id]; if (FBSpellCastCache[id] == nil) then FBPredict_TooltipText(id); end
if (c ~= nil) then return c or nil; end return FBSpellCastCache[id] or nil;
-- 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;
end end
-- Anteil des +Heilung-Werts, der auf diesen Zauber entfaellt -- Anteil des +Heilung-Werts, der auf diesen Zauber entfaellt
@@ -3682,25 +3752,55 @@ function FBHealBox_HealBonusFor(bookID)
return bonus * (secs / 3.5); return bonus * (secs / 3.5);
end 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 -- 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) function FBPredict_ExpectedDirect(spellName, rank, info, bookID)
local learned = FBPredict_Remembered("direct", spellName, rank); local learned = FBPredict_Remembered("direct", spellName, rank);
if (learned) then return learned; end if (learned) then return learned; end
if (not info) or (not info.direct) then return nil; 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 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) function FBHealBox_SpellRangeYards(id)
local c = FBSpellRangeCache[id]; if (not id) then return nil; end
if (c ~= nil) then return c or nil; end if (FBSpellRangeCache[id] == nil) then FBPredict_TooltipText(id); end
local txt = FBPredict_TooltipText and FBPredict_TooltipText(id) or ""; return FBSpellRangeCache[id] or nil;
local _, _, yd = string.find(txt, "(%d+)%s+[Yy]d"); end
if (not yd) then _, _, yd = string.find(txt, "(%d+)%s+[Mm]eter"); end
c = yd and tonumber(yd) or false; -- Abklingzeit eines Zaubers in Sekunden oder nil (rechts in Zeile 3)
FBSpellRangeCache[id] = c; function FBHealBox_SpellCooldownSecs(id)
return c or nil; if (not id) then return nil; end
if (FBSpellCDCache[id] == nil) then FBPredict_TooltipText(id); end
return FBSpellCDCache[id] or nil;
end end
-- Manapreis eines Zaubers aus dem Tooltip oder nil (Prozentkosten: nil) -- Manapreis eines Zaubers aus dem Tooltip oder nil (Prozentkosten: nil)
@@ -4168,7 +4268,7 @@ function FBHealBoxCreateAddonOptionFrame()
classIcon.tex:SetAllPoints(); classIcon.tex:SetAllPoints();
classIcon.tex:SetTexture(ClassIcon[FBClass]); classIcon.tex:SetTexture(ClassIcon[FBClass]);
classIcon.text = classIcon:CreateFontString(nil, "OVERLAY", "GameFontNormal"); 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:SetPoint("TOP", classIcon, "BOTTOM", 0, -2);
classIcon.text:SetTextColor(1, 1, 0.2, 1); classIcon.text:SetTextColor(1, 1, 0.2, 1);
@@ -4321,7 +4421,11 @@ function FBHealBoxCreateAddonOptionFrame()
getglobal(ScaleSlider:GetName() .. "High"):SetText(FBT("LARGE")); getglobal(ScaleSlider:GetName() .. "High"):SetText(FBT("LARGE"));
ScaleSlider:SetScript("OnValueChanged", function() ScaleSlider:SetScript("OnValueChanged", function()
HealBox.Scale = ScaleSlider:GetValue(); 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(); FBUpdateScaleSliderText();
end); end);
@@ -5077,6 +5181,18 @@ FBPredictTickInterval = {
FBHoTs = {}; -- [Name] = { [Zauber] = {rank, perTick, interval, expires} } FBHoTs = {}; -- [Name] = { [Zauber] = {rank, perTick, interval, expires} }
FBShields = {}; -- [Name] = { spell, rank, max, absorbed, 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) FBBuffTimers = {}; -- [Name] = { [Zauber] = { expires } } Buffs mit Laufzeit (eigene Casts, eigene Buffs)
FBBuffPresent = {}; -- [Name] = { [Zauber] = true } Buff (Textur) ist auf der Einheit FBBuffPresent = {}; -- [Name] = { [Zauber] = true } Buff (Textur) ist auf der Einheit
FBPredictDirect = nil; -- laufender Direktcast { target, spell, rank, amount, finish } 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 = CreateFrame("GameTooltip", "FBHealBoxScanTip", nil, "GameTooltipTemplate");
FBPredictTip:SetOwner(UIParent, "ANCHOR_NONE"); 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) function FBPredict_TooltipText(bookID)
if (not bookID) then return ""; end if (not bookID) then return ""; end
FBPredictTip:SetOwner(UIParent, "ANCHOR_NONE"); FBPredictTip:SetOwner(UIParent, "ANCHOR_NONE");
@@ -5104,34 +5267,26 @@ function FBPredict_TooltipText(bookID)
FBPredictTip:SetSpell(bookID, BOOKTYPE_SPELL); FBPredictTip:SetSpell(bookID, BOOKTYPE_SPELL);
local txt = ""; local txt = "";
local secs = nil; local secs, range, cd = nil, nil, nil;
local i = 1; local i = 1;
while (i <= 30) do while (i <= 30) do
local fs = getglobal("FBHealBoxScanTipTextLeft"..i); local fs = getglobal("FBHealBoxScanTipTextLeft"..i);
if (not fs or not fs:IsShown()) then break; end if (not fs or not fs:IsShown()) then break; end
local line = fs:GetText(); local line = fs:GetText();
if (line) then txt = txt.." "..line; end if (line) then txt = txt.." "..line; end
-- Die Zauberzeit steht rechts ("2.5 sec cast", "Instant cast"). Sie if (i <= FBTIP_HEAD_LINES) then
-- kommt bewusst nicht in txt: eine Zahl wie 2.5 wuerde die Muster local rline = nil;
-- 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
local rs = getglobal("FBHealBoxScanTipTextRight"..i); local rs = getglobal("FBHealBoxScanTipTextRight"..i);
if (rs and rs:IsShown()) then if (rs and rs:IsShown()) then rline = rs:GetText(); end
local rline = rs:GetText(); if (secs == nil) then secs = FBPredict_ParseCastTime(line) or FBPredict_ParseCastTime(rline); end
if (rline) then if (range == nil) then range = FBPredict_ParseRange(rline) or FBPredict_ParseRange(line); end
local _, _, v = string.find(rline, "([%d%.]+)%s+[Ss]ec%s+cast"); if (cd == nil) then cd = FBPredict_ParseCooldown(rline) or FBPredict_ParseCooldown(line); end
if (v) then secs = tonumber(v); end
if (secs == nil) and string.find(rline, "[Ii]nstant") then secs = 0; end
end
end
end end
i = i + 1; i = i + 1;
end end
if (FBSpellCastCache[bookID] == nil) then if (FBSpellCastCache[bookID] == nil) then FBSpellCastCache[bookID] = secs or false; end
FBSpellCastCache[bookID] = secs or false; if (FBSpellRangeCache[bookID] == nil) then FBSpellRangeCache[bookID] = range or false; end
end if (FBSpellCDCache[bookID] == nil) then FBSpellCDCache[bookID] = cd or false; end
return txt; return txt;
end end
@@ -5459,42 +5614,60 @@ end
-- Nur dann darf der beobachtete Wert dauerhaft gemerkt werden, sonst -- Nur dann darf der beobachtete Wert dauerhaft gemerkt werden, sonst
-- wuerde ein Rang-3-Cast von der Aktionsleiste die Schaetzung fuer den -- wuerde ein Rang-3-Cast von der Aktionsleiste die Schaetzung fuer den
-- Maximalrang verderben. -- 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) function FBPredict_StartHoT(unitName, spellName, rank, bookID, rankKnown)
local info = FBPredict_GetSpellInfo(bookID, spellName); local info = FBPredict_GetSpellInfo(bookID, spellName);
if (not info) or (not info.hot) then return false; end 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; local perTick = FBPredict_Remembered("tick", spellName, rank) or info.hot.perTick;
if (not FBHoTs[unitName]) then FBHoTs[unitName] = {}; end if (not FBHoTs[unitName]) then FBHoTs[unitName] = {}; end
FBHoTs[unitName][spellName] = { local e = {
rank = rank, rank = rank,
rankKnown = rankKnown, rankKnown = rankKnown,
perTick = perTick, perTick = perTick,
interval = info.hot.interval, 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 if (FBPredictDebug) then
local shown = spellName;
if (e.provisional) then shown = spellName.." (?)"; end
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[FBP]|r "..format(FBT("DBG_HOT"), 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 end
-- Nur gesicherte eigene Casts funken. Ein per Aura entdeckter HoT -- Nur gesicherte eigene Casts funken. Ein per Aura entdeckter HoT
-- koennte auch von einem anderen Heiler stammen. Der erste eigene -- koennte auch von einem anderen Heiler stammen. Der erste eigene
-- Combatlog-Tick holt das unten nach. -- Combatlog-Tick holt das unten nach.
if (rankKnown) then if (rankKnown) then
FBHoTs[unitName][spellName].commSent = true; e.commSent = true;
FBComm_SendHoT(spellName, unitName, info.hot.duration); FBComm_SendHoT(spellName, unitName, info.hot.duration);
end end
return true; -- Ein vorlaeufiger Eintrag aendert an der Anzeige nichts
return (rankKnown and true) or false;
end end
function FBPredict_StartShield(unitName, spellName, rank, bookID, rankKnown) function FBPredict_StartShield(unitName, spellName, rank, bookID, rankKnown)
local info = FBPredict_GetSpellInfo(bookID, spellName); local info = FBPredict_GetSpellInfo(bookID, spellName);
if (not info) or (not info.shield) then return false; end if (not info) or (not info.shield) then return false; end
-- Gelernter Wert nur, wenn er plausibel ist (hoechstens das -- Gelernter Wert nur bei einem eigenen, bestaetigten Cast und nur, wenn
-- FBPREDICT_ABSORB_MAX_FACTOR-fache des Tooltips); sonst Tooltip. -- er plausibel ist (hoechstens das FBPREDICT_ABSORB_MAX_FACTOR-fache des
local amount = FBPredict_Remembered("absorb", spellName, rank); -- 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 if (not amount) or (amount > info.shield.amount * FBPREDICT_ABSORB_MAX_FACTOR) then
amount = info.shield.amount; amount = info.shield.amount;
end end
@@ -5508,6 +5681,11 @@ function FBPredict_StartShield(unitName, spellName, rank, bookID, rankKnown)
absorbed = 0, absorbed = 0,
expires = GetTime() + info.shield.duration, 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 if (FBPredictDebug) then
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[FBP]|r "..format(FBT("DBG_SHIELD"), DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[FBP]|r "..format(FBT("DBG_SHIELD"),
@@ -5533,8 +5711,11 @@ function FBGetHoTHeal(unitName)
local now = GetTime(); local now = GetTime();
local sum = 0; local sum = 0;
for _, e in pairs(t) do for _, e in pairs(t) do
local n = FBPredict_TicksLeft(e, now); -- vorlaeufige (noch nicht als eigen bestaetigte) HoTs zaehlen nicht
if (n > 0) then sum = sum + (n * e.perTick); end if (not e.provisional) then
local n = FBPredict_TicksLeft(e, now);
if (n > 0) then sum = sum + (n * e.perTick); end
end
end end
return sum; return sum;
end end
@@ -5577,6 +5758,10 @@ function FBPredict_NoteCast(castString, targetName)
FBPredictCastTime = GetTime(); FBPredictCastTime = GetTime();
local base, rank = FBPredict_SplitCast(castString); 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 if (not FBPredictWatch[base]) then return; end
FBPredictPending = { FBPredictPending = {
@@ -5717,14 +5902,18 @@ function FBPredict_ScanUnit(unit)
if (w.hasHoT) then if (w.hasHoT) then
local tracked = FBHoTs[name] and FBHoTs[name][spellName]; local tracked = FBHoTs[name] and FBHoTs[name][spellName];
if (present and not tracked) then local foreign = FBHoTForeign[name] and FBHoTForeign[name][spellName];
-- z.B. von der Aktionsleiste: hoechster bekannter Rang, if (present and not tracked and not foreign) then
-- der erste Combatlog-Tick korrigiert den Wert ohnehin -- 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; dirty = FBPredict_StartHoT(name, spellName, w.rank, w.bookID) or dirty;
elseif (tracked and not present) then elseif (tracked and not present) then
FBHoTs[name][spellName] = nil; FBHoTs[name][spellName] = nil;
dirty = true; if (not tracked.provisional) then dirty = true; end
end end
-- Buff weg: die Fremdmarke gilt nur, solange er da ist
if (foreign and not present) then FBHoTForeign[name][spellName] = nil; end
end end
if (w.hasShield) then if (w.hasShield) then
@@ -5820,16 +6009,42 @@ function FBPredict_ResolveVictim(event, msg)
return nil; return nil;
end 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) function FBPredict_OnTick(unitName, amount, spellName)
if (not unitName) or (not amount) or (not spellName) then return; end if (not unitName) or (not amount) or (not spellName) then return; end
FBLOS_Clear(unitName); FBLOS_Clear(unitName);
local t = FBHoTs[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]; local e = t[spellName];
-- "... from your Renew" beweist: der HoT ist von uns. Falls er ueber -- "... from your Renew" beweist: der HoT ist von uns. Ein am Buff
-- die Aktionsleiste kam und noch nicht gefunkt wurde, jetzt nachholen. -- 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 if (not e.commSent) then
e.commSent = true; e.commSent = true;
FBComm_SendHoT(spellName, unitName, e.expires - GetTime()); FBComm_SendHoT(spellName, unitName, e.expires - GetTime());
@@ -6019,19 +6234,36 @@ FBPredictFrame:SetScript("OnEvent", function()
FBLOS_OnError(arg1); FBLOS_OnError(arg1);
elseif (event == "SPELLCAST_START") then 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)); FBPredict_CastStart(arg1, tonumber(arg2));
elseif (event == "SPELLCAST_STOP") then elseif (event == "SPELLCAST_STOP") then
-- Erfolgreich beendet: HealComm-Empfaenger lassen den Eintrag -- Erfolgreich beendet: HealComm-Empfaenger lassen den Eintrag
-- selbst auslaufen, hier wird bewusst kein Healstop gefunkt. -- selbst auslaufen, hier wird bewusst kein Healstop gefunkt.
FBLOSCandidate = nil; -- Cast ging durch, die Sicht war da
FBPredict_ConfirmBuffRefresh(); FBPredict_ConfirmBuffRefresh();
FBPredict_CastEnd(); FBPredict_CastEnd();
elseif (event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED") then 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 if (FBPredictDirect) then FBComm_SendHealStop(); end
FBPredict_CastEnd(); FBPredict_CastEnd();
elseif (event == "SPELLCAST_DELAYED") then elseif (event == "SPELLCAST_DELAYED") then
if (FBLOSCandidate and arg1) then
FBLOSCandidateUntil = FBLOSCandidateUntil + (tonumber(arg1) or 0) / 1000;
end
if (FBPredictDirect and arg1) then if (FBPredictDirect and arg1) then
FBPredictDirect.finish = FBPredictDirect.finish + (tonumber(arg1) / 1000); FBPredictDirect.finish = FBPredictDirect.finish + (tonumber(arg1) / 1000);
FBComm_SendHealDelay(arg1); FBComm_SendHealDelay(arg1);
@@ -6114,8 +6346,22 @@ function FBPredict_OnUpdate(elapsed)
for spellName, e in pairs(spells) do for spellName, e in pairs(spells) do
if (now >= e.expires) then if (now >= e.expires) then
spells[spellName] = nil; spells[spellName] = nil;
names[uname] = true; if (not e.provisional) then
dirty = true; 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 else
anyLeft = true; anyLeft = true;
local n = FBPredict_TicksLeft(e, now); local n = FBPredict_TicksLeft(e, now);
@@ -6139,6 +6385,11 @@ function FBPredict_OnUpdate(elapsed)
end end
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 for uname, casters in pairs(FBCommHeals) do
local anyLeft = false; local anyLeft = false;
for caster, info in pairs(casters) do for caster, info in pairs(casters) do
@@ -6417,7 +6668,7 @@ SlashCmdList["FBHEALPREDICT"] = function(msg)
-- Optionsfenster und Diagnose waeren leer. -- Optionsfenster und Diagnose waeren leer.
if (FBAddonSuppressed) then if (FBAddonSuppressed) then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r " 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"); DEFAULT_CHAT_FRAME:AddMessage("|cFFAAAAAA"..FBT("CLASS_BLOCKED_HINT").."|r");
return; return;
end end
@@ -6539,6 +6790,14 @@ SlashCmdList["FBHEALPREDICT"] = function(msg)
parts = parts.." "..FBT("FBP_SHIELD").."="..info.shield.amount parts = parts.." "..FBT("FBP_SHIELD").."="..info.shield.amount
.."/"..info.shield.duration.."s"; .."/"..info.shield.duration.."s";
end 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); DEFAULT_CHAT_FRAME:AddMessage(" "..spellName.." ("..tostring(w.rank)..")"..parts);
end end
end end
@@ -6552,7 +6811,10 @@ SlashCmdList["FBHEALPREDICT"] = function(msg)
end end
for name, spells in pairs(FBHoTs) do for name, spells in pairs(FBHoTs) do
for spellName, e in pairs(spells) 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").." " ..FBPredict_TicksLeft(e, now).." "..FBT("FBP_TICKSOF").." "
..math.floor(e.perTick)); ..math.floor(e.perTick));
end end
+2 -2
View File
@@ -1,9 +1,9 @@
## Interface: 11200 ## 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: 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. ## 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 ## Author: Dourd (Argent Dawn EU) | Vanilla-Port & Erweiterung 09/2026: Mquadrat
## Version: 1.4.6 ## Version: 1.4.6.1
## SavedVariablesPerCharacter: HealBox ## SavedVariablesPerCharacter: HealBox
## OptionalDeps: SuperWoW ## OptionalDeps: SuperWoW
+19 -3
View File
@@ -290,6 +290,7 @@ FBRaidTestMode = 0; -- 0 | 20 | 40
FBRaidTestGhosts = {}; -- [i] = Geist-Datensatz (Testmodus) FBRaidTestGhosts = {}; -- [i] = Geist-Datensatz (Testmodus)
FBRaidTickAccum = 0; FBRaidTickAccum = 0;
FBRaidDragging = false; FBRaidDragging = false;
FBRaidAppliedScale = nil; -- zuletzt gesetzte Skalierung (FBRaid_LayoutAll)
function FBRaid_Cfg() function FBRaid_Cfg()
if (not HealBox.Raid) then FBRaid_ApplyDefaults(); end if (not HealBox.Raid) then FBRaid_ApplyDefaults(); end
@@ -664,7 +665,12 @@ function FBRaid_CreateCell(g, pos)
if (FBDragSpell) then if (FBDragSpell) then
local side = "L"; local side = "L";
if (arg1 == "RightButton" or IsShiftKeyDown()) then side = "R"; end 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 end
if (FBRaidTestMode > 0) then if (FBRaidTestMode > 0) then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..FBT("RAID_TEST_CLICK")); DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..FBT("RAID_TEST_CLICK"));
@@ -827,7 +833,16 @@ function FBRaid_LayoutAll()
local titleH = 0; local titleH = 0;
if (cfg.ShowTitle == 1) then titleH = FBRAID_TITLE_H + 2; FBRaidFrame.Title:Show(); else FBRaidFrame.Title:Hide(); end 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; local perRow = cfg.GroupsPerRow;
if (perRow < 1) then perRow = 1; end if (perRow < 1) then perRow = 1; end
@@ -1093,7 +1108,8 @@ function FBRaid_UpdateSpellTimers(now)
for pos = 1, FBRAID_PER_GROUP do for pos = 1, FBRAID_PER_GROUP do
local c = FBRaidCells[g][pos]; local c = FBRaidCells[g][pos];
if (c) then 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 -- Nichts anzuzeigen und nichts mehr wegzuraeumen: Zelle
-- ueberspringen. Vorher lief die Buttonschleife auch dann -- ueberspringen. Vorher lief die Buttonschleife auch dann
-- durch, im Vierzigerraid 160 Leeraufrufe fuenfmal je -- durch, im Vierzigerraid 160 Leeraufrufe fuenfmal je