From de4daab0390007540dbcedce6af91e2dcc035312 Mon Sep 17 00:00:00 2001 From: QbiXibQ Date: Sat, 5 Sep 2026 20:53:40 +0200 Subject: [PATCH] Add files via upload --- FBHealBox.lua | 1276 ++++++++++++++++++++++++++++++++++++++++-- FBHealBox.toc | 10 +- FBHealBox_Damage.lua | 617 ++++++++++++++++++++ FBHealBox_Raid.lua | 312 ++++++++++- FBHealBox_Ticker.lua | 549 ++++++++++++++++++ 5 files changed, 2690 insertions(+), 74 deletions(-) create mode 100644 FBHealBox_Damage.lua create mode 100644 FBHealBox_Ticker.lua diff --git a/FBHealBox.lua b/FBHealBox.lua index 0d60534..070994d 100644 --- a/FBHealBox.lua +++ b/FBHealBox.lua @@ -17,6 +17,12 @@ -- Anzeige, Debuff-Icon mit Stackzahl -- * v1.4.2: Hook-Schnittstelle fuer Module (FBHealBox_RegisterHook, -- FBHealBox_AddOptionsTab). Der Raidmodus lebt in FBHealBox_Raid.lua. +-- * v1.4.3: Mana-Ticker-Modul (FBHealBox_Ticker.lua), Smart-Damage-Modul +-- (FBHealBox_Damage.lua, Abrangen von Angriffszaubern), Reiterknoepfe +-- schmaler, damit vier Reiter ins Optionsfenster passen, Smart Healing +-- (automatisches Abrangen je Ziel, standardmaessig aus), Cooldown-Uhr auf den Buttons, roter Rahmen fuer +-- "wer wird angegriffen", HoT- und Schild-Restzeit auf dem jeweiligen +-- Button (inkl. Geschwaechte Seele). -- -- Ehre wem Ehre gebuehrt: Aufbau, Namensplaketten und Grundidee stammen -- aus dem Original. @@ -53,6 +59,12 @@ HealBox = { DebuffIcon = 1, -- Debuff-Icon neben dem Namen LOSIcon = 1, -- Sichtlinien-Abzeichen BuffWatchPets = 0, -- Buff-Wache auch fuer Begleiter + SmartRank = 0, -- Smart Healing (bewusst aus: Overheal kann gewollt sein) + SmartMargin = 20, -- Sicherheitsaufschlag in Prozent + Cooldowns = 1, -- Cooldown-Uhr auf den Buttons + AggroMark = 1, -- roter Rahmen fuer den Angegriffenen + SpellTimers = 1, -- HoT-/Schild-Restzeit auf den Buttons + BuffIcons = 1, -- Buff-Icons mit Uhr im Lebensbalken PlateLeft = "target", -- Klick auf die Plakette: target | menu | move | none PlateRight = "target", }; @@ -61,7 +73,7 @@ HealBox = { -- feuert ADDON_LOADED fuer uns. FBADDON_NAME = "Heal Box Vanilla"; FBADDON_FOLDER = "FBHealBox"; -HealBoxVersion = "|cFFFFFF00v1.4.2|r"; +HealBoxVersion = "|cFFFFFF00v1.4.3|r"; -- ========================================================================== -- [ Lokalisierung / Localization ] @@ -101,9 +113,22 @@ FBLocale["enUS"] = { RIGHTCLICK = "Right-click spell", RIGHTCLICK_TIP = "Gives every button a second spell on right click (e.g. Flash Heal left, Greater Heal right). Off by default. When on, a second column appears above and a small icon in the corner of each button shows the right-click spell. Drop a spell with the right mouse button or with Shift held to fill this side.", TT_RIGHT = "Right click", + TT_BUFF_UNKNOWN = "remaining time unknown (not your cast)", DROP_SET = "Button %d: |cFFFFFFFF%s|r (dragged from the spellbook)", DROP_SET_R = "Button %d, right click: |cFFFFFFFF%s|r (dragged from the spellbook)", DROP_UNKNOWN = "Could not identify the dragged spell.", + SMARTRANK = "Smart Healing", + SMARTRANK_TIP = "What it does: on click, instead of the assigned rank the button casts the lowest rank of the same spell whose expected heal covers the target's missing health (minus healing already on the way) plus the safety margin. Saves mana and overhealing. Rules: never above the assigned rank, direct heals only (HoTs and shields are unaffected), and always the assigned rank below 30 % health. Downsides: the expected heal is an estimate from the tooltip or from learned values and ignores crits; with burst damage, or when you deliberately want to overheal as a buffer (a tank before a big hit), the lower rank can fall short. Turn it off whenever overhealing is what you want. Every decision is logged with /fbp debug.", + SMART_MARGIN = "Safety margin: |cFFFFFFFF%s %%", + COOLDOWNS = "Cooldowns on buttons", + COOLDOWNS_TIP = "Shows the cooldown sweep on every button (Nature's Swiftness, Inner Focus, Lay on Hands, shield cooldown). The global cooldown is not shown.", + AGGRO = "Mark who is attacked", + AGGRO_TIP = "Red border on the plate or cell of the member your hostile target is currently targeting. Checked five times a second.", + BUFFICONS = "Buff icons left of the bar", + BUFFICONS_TIP = "Buffs with a duration that sit on your buttons (Fortitude, Divine Spirit, Fear Ward, ...) appear as small icons on the outer left side of the plate while they are up. Like a minute hand, the icon turns black and white clockwise from twelve o'clock as the time runs out: full colour when fresh, right half grey at half time, three quarters grey at a quarter left. Exact on yourself, counted from your own cast on others; a buff cast by someone else stays fully coloured (time unknown). /fbp buffs shows what is tracked.", + TIMERS = "HoT and shield timers", + TIMERS_TIP = "Each button shows the remaining seconds of your own HoT or shield of that spell on that unit: green for HoTs, blue for the shield, red for Weakened Soul after the shield. Buffs with a duration are shown as icons in the health bar instead (see Buff icons).", + DBG_SMARTRANK = "Auto-downrank: %s -> %s (missing %d, expected %d)", DROP_HINT = "Drag spells from the spellbook onto a button. Right mouse button or Shift while dropping fills the right-click side.", STATE_DEAD = "Dead", STATE_GHOST = "Ghost", @@ -176,7 +201,8 @@ FBLocale["enUS"] = { FBP_INCOMING = "Incoming", FBP_DEBUG = "Debug:", FBP_RESET = "Learned values discarded.", - FBP_COMMANDS = "Commands: /fbp config (options window), /fbp test (test mode), /fbp debug, /fbp reset", + FBP_COMMANDS = "Commands: /fbp config (options window), /fbp test (test mode), /fbp buffs (buff diagnostics), /fbp debug, /fbp reset", + FBP_SMART = "Smart Healing: %s (safety margin %d %%)", DBG_HOT = "HoT %s on %s: %d per tick, %ds", DBG_SHIELD = "Shield %s on %s: %d absorb, %ds", @@ -207,9 +233,22 @@ FBLocale["deDE"] = { RIGHTCLICK = "Rechtsklick-Zauber", RIGHTCLICK_TIP = "Gibt jedem Button einen zweiten Zauber per Rechtsklick (z. B. Blitzheilung links, Grosse Heilung rechts). Standardmaessig aus. Eingeschaltet erscheint oben eine zweite Spalte, und ein kleines Icon in der Ecke jedes Buttons zeigt den Rechtsklick-Zauber. Zauber mit rechter Maustaste oder gehaltener Shift-Taste ablegen, um diese Seite zu fuellen.", TT_RIGHT = "Rechtsklick", + TT_BUFF_UNKNOWN = "Restzeit unbekannt (nicht dein Cast)", DROP_SET = "Button %d: |cFFFFFFFF%s|r (aus dem Zauberbuch gezogen)", DROP_SET_R = "Button %d, Rechtsklick: |cFFFFFFFF%s|r (aus dem Zauberbuch gezogen)", DROP_UNKNOWN = "Der gezogene Zauber liess sich nicht erkennen.", + SMARTRANK = "Smart Healing", + SMARTRANK_TIP = "Was es tut: Beim Klick wirkt der Button statt des belegten Rangs den niedrigsten Rang desselben Zaubers, dessen erwartete Heilung das fehlende Leben des Ziels (abzueglich bereits eingehender Heilung) plus Sicherheitsaufschlag deckt. Spart Mana und Overheal. Regeln: nie ueber dem belegten Rang, nur Direktheilungen (HoTs und Schilde bleiben unberuehrt), unter 30 % Leben immer der belegte Rang. Nachteile: Die erwartete Heilung ist eine Schaetzung aus Tooltip oder gelernten Werten und kennt keine Crits; bei Schadensspitzen oder wenn du bewusst ueberheilen willst (Tank vor einem grossen Treffer) kann der kleinere Rang zu wenig sein. Ausschalten, wann immer Overheal gewollt ist. Jede Entscheidung steht mit /fbp debug im Chat.", + SMART_MARGIN = "Sicherheitsaufschlag: |cFFFFFFFF%s %%", + COOLDOWNS = "Cooldowns auf den Buttons", + COOLDOWNS_TIP = "Zeigt die Cooldown-Uhr auf jedem Button (Naturschnelligkeit, Innerer Fokus, Handauflegung, Schild-Cooldown). Der globale Cooldown wird nicht angezeigt.", + AGGRO = "Angegriffenen markieren", + AGGRO_TIP = "Roter Rahmen auf der Plakette oder Zelle des Mitglieds, das dein feindliches Ziel gerade im Ziel hat. Fuenfmal je Sekunde geprueft.", + BUFFICONS = "Buff-Icons links am Balken", + BUFFICONS_TIP = "Buffs mit Laufzeit, die auf deinen Buttons liegen (Seelenstaerke, Goettlicher Willen, Furchtzauberschutz, ...), erscheinen als kleine Icons aussen links neben der Plakette, solange sie wirken. Wie ein Minutenzeiger wird das Icon mit ablaufender Zeit im Uhrzeigersinn ab zwoelf Uhr schwarz-weiss: frisch ganz farbig, bei halber Zeit die rechte Haelfte grau, bei einem Viertel Rest drei Viertel grau. Exakt bei dir selbst, ab deinem eigenen Cast bei anderen; ein fremd gewirkter Buff bleibt ganz farbig (Zeit unbekannt). /fbp buffs zeigt, was verfolgt wird.", + TIMERS = "HoT- und Schild-Timer", + TIMERS_TIP = "Jeder Button zeigt die Restsekunden deines eigenen HoTs oder Schilds dieses Zaubers auf dieser Einheit: gruen fuer HoTs, blau fuer den Schild, rot fuer Geschwaechte Seele nach dem Schild. Buffs mit Laufzeit erscheinen stattdessen als Icons im Lebensbalken (siehe Buff-Icons).", + DBG_SMARTRANK = "Abrangen: %s -> %s (fehlend %d, erwartet %d)", DROP_HINT = "Zauber aus dem Zauberbuch auf einen Button ziehen. Rechte Maustaste oder Shift beim Ablegen fuellt die Rechtsklick-Seite.", STATE_DEAD = "Tot", STATE_GHOST = "Geist", @@ -282,7 +321,8 @@ FBLocale["deDE"] = { FBP_INCOMING = "Fremdheilung", FBP_DEBUG = "Debug:", FBP_RESET = "Gelernte Werte verworfen.", - FBP_COMMANDS = "Befehle: /fbp config (Optionsfenster), /fbp test (Testmodus), /fbp debug, /fbp reset", + FBP_COMMANDS = "Befehle: /fbp config (Optionsfenster), /fbp test (Testmodus), /fbp buffs (Buff-Diagnose), /fbp debug, /fbp reset", + FBP_SMART = "Smart Healing: %s (Sicherheitsaufschlag %d %%)", DBG_HOT = "HoT %s auf %s: %d pro Tick, %ds", DBG_SHIELD = "Schild %s auf %s: %d Absorb, %ds", @@ -296,7 +336,11 @@ FBL = nil; -- Client-Sprache als Vorgabe function FBDetectLocale() - if (GetLocale and GetLocale() == "deDE") then return "deDE"; end + local loc = GetLocale and GetLocale(); + if (loc == "deDE") then return "deDE"; end + if (loc == "esES" or loc == "esMX") then return "esES"; end + if (loc == "frFR") then return "frFR"; end + if (loc == "itIT") then return "itIT"; end return "enUS"; end @@ -316,6 +360,339 @@ function FBSetLocale(code, apply) if (apply) then FBHealBox_ApplyLocale(); end end +FBLocale["esES"] = { + LANG_NAME = "Español", + LANGUAGE = "Idioma", + LOADED = "cargado|r. El botón del minimapa o /fbp config abre las opciones, /fbp muestra la predicción de curación.", + SUPERWOW = " |cFF55FF55[SuperWoW detectado]|r", + CREDITS = "|cFFAAAAAAOriginal de Dourd (Argent Dawn EU), portado a Vanilla y ampliado en 09/2026 por Mquadrat|r", + TT_NO_SPELL = "|cFFFFFFFFSin hechizo\n|cFF00FF00Elige un hechizo en la ventana de opciones.", + TT_TARGET = "Objetivo", + NOT_IN_GROUP = " no está en tu grupo.", + SELECT_SPELL = "Elegir hechizo...", + BUTTON = "Botón", + TAB_BUTTONS = "Botones", + TAB_GENERAL = "General", + COL_LEFT = "Clic izquierdo", + COL_RIGHT = "Clic derecho", + RIGHTCLICK = "Hechizo de clic derecho", + RIGHTCLICK_TIP = "Da a cada botón un segundo hechizo con el clic derecho (p. ej. Sanación relámpago a la izquierda, Sanación superior a la derecha). Desactivado por defecto. Al activarlo aparece una segunda columna arriba y un pequeño icono en la esquina de cada botón muestra el hechizo del clic derecho. Suelta un hechizo con el botón derecho o con Mayús pulsado para rellenar este lado.", + TT_RIGHT = "Clic derecho", + TT_BUFF_UNKNOWN = "tiempo restante desconocido (no es tu lanzamiento)", + DROP_SET = "Botón %d: |cFFFFFFFF%s|r (arrastrado desde el libro de hechizos)", + DROP_SET_R = "Botón %d, clic derecho: |cFFFFFFFF%s|r (arrastrado desde el libro de hechizos)", + DROP_UNKNOWN = "No se pudo identificar el hechizo arrastrado.", + SMARTRANK = "Smart Healing", + SMARTRANK_TIP = "Qué hace: al hacer clic, en lugar del rango asignado el botón lanza el rango más bajo del mismo hechizo cuya curación esperada cubra la vida que le falta al objetivo (menos la curación ya en camino) más el margen de seguridad. Ahorra maná y sobrecuración. Reglas: nunca por encima del rango asignado, solo curaciones directas (HoT y escudos no se ven afectados) y siempre el rango asignado por debajo del 30 % de vida. Desventajas: la curación esperada es una estimación del tooltip o de valores aprendidos y no cuenta los críticos; con daño en ráfaga, o cuando quieres sobrecurar a propósito como colchón (un tanque antes de un golpe fuerte), el rango menor puede quedarse corto. Desactívalo siempre que quieras sobrecurar. Cada decisión se registra con /fbp debug.", + SMART_MARGIN = "Margen de seguridad: |cFFFFFFFF%s %%", + COOLDOWNS = "Reutilización en botones", + COOLDOWNS_TIP = "Muestra el barrido de reutilización en cada botón (Rapidez de la naturaleza, Enfoque interno, Imposición de manos, reutilización del escudo). La reutilización global no se muestra.", + AGGRO = "Marcar al atacado", + AGGRO_TIP = "Borde rojo en la placa o celda del miembro al que tu objetivo hostil está apuntando. Se comprueba cinco veces por segundo.", + BUFFICONS = "Iconos de beneficios (izq.)", + BUFFICONS_TIP = "Los beneficios con duración que están en tus botones (Entereza, Espíritu divino, Custodia contra el miedo, ...) aparecen como pequeños iconos en el lado exterior izquierdo de la placa mientras están activos. Como una aguja de minutos, el icono se vuelve blanco y negro en el sentido de las agujas del reloj desde las doce a medida que se agota el tiempo: todo en color al principio, mitad derecha gris a mitad de tiempo, tres cuartos gris cuando queda un cuarto. Exacto en ti mismo, contado desde tu propio lanzamiento en los demás; un beneficio lanzado por otro se queda en color (tiempo desconocido). /fbp buffs muestra qué se sigue.", + TIMERS = "Temporizadores HoT y escudo", + TIMERS_TIP = "Cada botón muestra los segundos restantes de tu propio HoT o escudo de ese hechizo en esa unidad: verde para HoT, azul para el escudo, rojo para Alma debilitada tras el escudo. Los beneficios con duración se muestran como iconos en la barra de vida (ver Iconos de beneficios).", + DBG_SMARTRANK = "Reducción de rango: %s -> %s (faltan %d, esperado %d)", + DROP_HINT = "Arrastra hechizos del libro de hechizos a un botón. El botón derecho o Mayús al soltar rellena el lado del clic derecho.", + STATE_DEAD = "Muerto", + STATE_GHOST = "Fantasma", + STATE_OFFLINE = "Desconectado", + PLATE_LEFT = "Clic izquierdo en la placa", + PLATE_RIGHT = "Clic derecho en la placa", + PLATE_TIP = "Qué hace un clic en una placa de nombre (nombre o barra de vida). Mayús + arrastrar con el izquierdo siempre mueve la pantalla, sea cual sea el ajuste.", + ACT_TARGET = "Seleccionar", + ACT_MENU = "Menú de unidad", + ACT_MOVE = "Mover la pantalla", + ACT_NONE = "Nada", + LOSICON = "Línea de visión", + LOSICON_TIP = "Muestra un ojo en la esquina de la placa mientras la unidad está fuera de tu línea de visión. Con el mod de cliente UnitXP se comprueba en vivo; sin él, el addon recuerda un error de \"fuera de línea de visión\" durante unos segundos tras intentar curar a esa unidad y lo borra en cuanto empieza un lanzamiento sobre ella o llega una curación.", + DEBUFFICON = "Icono de perjuicio", + DEBUFFICON_TIP = "Muestra el icono del primer perjuicio que tu clase puede quitar junto al nombre, con su número de acumulaciones. La barra de vida sigue tomando además el color del perjuicio.", + MENU_NO_SPELL = "|cFF999999Sin hechizo|r", + RANK_DEFAULT = "Predeterminado", + PANEL_SUB = "Opciones de %s.\nElige cuántos botones mostrar\ny qué hechizo lanza cada botón.", + SHOW_BUTTONS = "Mostrar |cFFFFFFFF%s|r botones", + SCALE = "Escala del marco: |cFFFFFFFF%s", + SMALL = "Pequeño", + LARGE = "Grande", + BTN_SPACING = "Separación de botones: |cFFFFFFFF%s px", + ROW_SPACING = "Separación de filas: |cFFFFFFFF%s px", + ATTACH = "Marcos de grupo de Blizzard", + ATTACH_TIP = "Ancla los botones de curación a los marcos de grupo predeterminados de Blizzard en lugar de usar las placas móviles propias del addon.", + COMM = "Sincronización HealComm", + COMM_TIP = "Emite tus curaciones en el formato HealComm para que Puppeteer, pfUI, Luna y otros puedan mostrarlas, e incorpora a tu propia predicción las curaciones anunciadas por otros sanadores.", + MANABAR = "Barra de maná", + MANABAR_TIP = "Muestra una fina barra de maná azul en el borde inferior de la barra de vida, solo para unidades que usan maná (no ira, enfoque ni energía).", + SHOWPETS = "Mostrar mascotas", + SHOWPETS_TIP = "Añade una placa para cada mascota del grupo (cazador, brujo) justo debajo de su dueño, con los mismos botones de curación.", + TESTMODE = "Modo de prueba", + TESTMODE_TIP = "Rellena la pantalla con jugadores y mascotas fantasma para que puedas organizarlo todo sin estar en grupo. Los fantasmas muestran vida, maná, un escudo, curación entrante y un perjuicio disipable. No se guarda, siempre apagado al iniciar sesión.", + TEST_ON = "Modo de prueba |cFF00FF00activado|r, jugadores fantasma activos. Los botones no lanzan sobre fantasmas.", + TEST_OFF = "Modo de prueba |cFFFF0000desactivado|r.", + TEST_CLICK = "Modo de prueba: sin lanzamiento sobre un jugador fantasma.", + CLASSCOLORS = "Colores de clase", + CLASSCOLORS_TIP = "Colorea el nombre de cada placa con el color de clase (estilo Healium). Las mascotas conservan su nombre azul claro.", + RANGEFADE = "Atenuación por distancia", + RANGEFADE_TIP = "Atenúa toda la placa, botones incluidos, a media transparencia cuando la unidad está fuera del alcance de tu primer hechizo asignado (o a más de 28 metros si no hay hechizo asignado).", + BUFFWATCH = "Vigilancia de beneficio", + BUFFWATCH_TIP = "Elige uno de tus beneficios. Cada placa cuya unidad carezca de ese beneficio recibe un borde naranja. La versión de grupo también cuenta (Rezo de entereza para Palabra de poder: entereza, Don de lo Salvaje para Marca de lo Salvaje, Bendiciones superiores, ...).", + BUFFWATCH_NONE = "ninguno", + BUFFWATCH_PETS = "Vigilancia de beneficio: mascotas", + BUFFWATCH_PETS_TIP = "Marca también las mascotas a las que les falta el beneficio vigilado. Desactivado por defecto, porque las mascotas rara vez reciben Entereza o Bendiciones y sus placas estarían naranjas casi siempre.", + MENU_NO_BUFF = "|cFF999999Sin vigilancia de beneficio|r", + LANG_TIP = "Cambia todos los textos del addon. Surte efecto de inmediato, sin recargar.", + ABOUT = "%s %s |cFFAAAAAA(original de Dourd, UI Overhauled)|r\n|cFFAAAAAAPortado a Vanilla y ampliado en 09/2026 por Mquadrat|r", + MM_TIP = "Izquierdo: opciones\nMantener derecho: mover este botón\nMayús + izquierdo: mostrar/ocultar la pantalla", + FBP_WATCHED = "Hechizos leídos del libro de hechizos:", + FBP_ACTIVE = "Activos ahora:", + FBP_DIRECT = "directo", + FBP_SHIELD = "escudo", + FBP_LEARNED = "aprendido", + FBP_EVERY = "cada", + FBP_TICKSOF = "pulsos de", + FBP_CAST = "Lanzamiento", + FBP_ON = "sobre", + FBP_OF = "de", + FBP_FOR = "por", + FBP_SYNC = "Sincronización HealComm:", + FBP_STATE_ON = "activada", + FBP_STATE_OFF = "desactivada", + FBP_INCOMING = "Entrante", + FBP_DEBUG = "Depuración:", + FBP_RESET = "Valores aprendidos descartados.", + FBP_COMMANDS = "Comandos: /fbp config (ventana de opciones), /fbp test (modo de prueba), /fbp buffs (diagnóstico de beneficios), /fbp debug, /fbp reset", + FBP_SMART = "Smart Healing: %s (margen de seguridad %d %%)", + DBG_HOT = "HoT %s sobre %s: %d por pulso, %ds", + DBG_SHIELD = "Escudo %s sobre %s: %d absorción, %ds", + DBG_CAST = "Lanzamiento %s sobre %s: %d", + DBG_TICK = "Pulso corregido: %s = %d", + DBG_HEAL = "Curación %s = %d (estimación ahora %d)", + DBG_ABSORB = "Absorción %d en %s (quedan %d)", +}; + +FBLocale["frFR"] = { + LANG_NAME = "Français", + LANGUAGE = "Langue", + LOADED = "chargé|r. Le bouton de la minicarte ou /fbp config ouvre les options, /fbp affiche la prédiction de soins.", + SUPERWOW = " |cFF55FF55[SuperWoW détecté]|r", + CREDITS = "|cFFAAAAAAOriginal de Dourd (Argent Dawn EU), porté sur Vanilla et étendu en 09/2026 par Mquadrat|r", + TT_NO_SPELL = "|cFFFFFFFFAucun sort\n|cFF00FF00Choisissez un sort dans la fenêtre des options.", + TT_TARGET = "Cible", + NOT_IN_GROUP = " n'est pas dans votre groupe.", + SELECT_SPELL = "Choisir un sort...", + BUTTON = "Bouton", + TAB_BUTTONS = "Boutons", + TAB_GENERAL = "Général", + COL_LEFT = "Clic gauche", + COL_RIGHT = "Clic droit", + RIGHTCLICK = "Sort du clic droit", + RIGHTCLICK_TIP = "Donne à chaque bouton un second sort au clic droit (p. ex. Soins rapides à gauche, Soins supérieurs à droite). Désactivé par défaut. Une fois activé, une seconde colonne apparaît au-dessus et une petite icône dans le coin de chaque bouton montre le sort du clic droit. Déposez un sort avec le bouton droit ou en maintenant Maj pour remplir ce côté.", + TT_RIGHT = "Clic droit", + TT_BUFF_UNKNOWN = "temps restant inconnu (pas votre lancement)", + DROP_SET = "Bouton %d : |cFFFFFFFF%s|r (glissé depuis le grimoire)", + DROP_SET_R = "Bouton %d, clic droit : |cFFFFFFFF%s|r (glissé depuis le grimoire)", + DROP_UNKNOWN = "Impossible d'identifier le sort glissé.", + SMARTRANK = "Smart Healing", + SMARTRANK_TIP = "Ce que ça fait : au clic, au lieu du rang assigné, le bouton lance le rang le plus bas du même sort dont le soin attendu couvre les points de vie manquants de la cible (moins les soins déjà en route) plus la marge de sécurité. Économise du mana et du sursoin. Règles : jamais au-dessus du rang assigné, soins directs uniquement (les HoT et les boucliers ne sont pas concernés), et toujours le rang assigné sous 30 % de vie. Inconvénients : le soin attendu est une estimation tirée de l'infobulle ou des valeurs apprises et ignore les critiques ; en cas de dégâts en rafale, ou quand vous voulez volontairement sursoigner comme tampon (un tank avant un gros coup), le rang inférieur peut être insuffisant. Désactivez-le dès que vous voulez sursoigner. Chaque décision est consignée avec /fbp debug.", + SMART_MARGIN = "Marge de sécurité : |cFFFFFFFF%s %%", + COOLDOWNS = "Recharges sur les boutons", + COOLDOWNS_TIP = "Affiche le balayage du temps de recharge sur chaque bouton (Rapidité de la nature, Focalisation intérieure, Imposition des mains, recharge du bouclier). Le temps de recharge global n'est pas affiché.", + AGGRO = "Marquer la cible attaquée", + AGGRO_TIP = "Bordure rouge sur la plaque ou la cellule du membre que votre cible hostile vise actuellement. Vérifié cinq fois par seconde.", + BUFFICONS = "Icônes de buffs (gauche)", + BUFFICONS_TIP = "Les buffs à durée présents sur vos boutons (Robustesse, Esprit divin, Gardien de peur, ...) apparaissent sous forme de petites icônes sur le côté extérieur gauche de la plaque tant qu'ils sont actifs. Comme une aiguille des minutes, l'icône passe en noir et blanc dans le sens horaire à partir de midi au fur et à mesure que le temps s'écoule : entièrement en couleur au début, moitié droite grise à mi-temps, trois quarts gris quand il reste un quart. Exact sur vous-même, compté depuis votre propre lancement sur les autres ; un buff lancé par quelqu'un d'autre reste en couleur (temps inconnu). /fbp buffs montre ce qui est suivi.", + TIMERS = "Minuteurs HoT et bouclier", + TIMERS_TIP = "Chaque bouton affiche les secondes restantes de votre propre HoT ou bouclier de ce sort sur cette unité : vert pour les HoT, bleu pour le bouclier, rouge pour Âme affaiblie après le bouclier. Les buffs à durée sont affichés sous forme d'icônes dans la barre de vie (voir Icônes de buffs).", + DBG_SMARTRANK = "Rang abaissé : %s -> %s (manque %d, attendu %d)", + DROP_HINT = "Glissez des sorts du grimoire sur un bouton. Le bouton droit ou Maj au dépôt remplit le côté du clic droit.", + STATE_DEAD = "Mort", + STATE_GHOST = "Fantôme", + STATE_OFFLINE = "Hors ligne", + PLATE_LEFT = "Clic gauche sur la plaque", + PLATE_RIGHT = "Clic droit sur la plaque", + PLATE_TIP = "Ce que fait un clic sur une plaque (nom ou barre de vie). Maj + glisser avec le bouton gauche déplace toujours l'affichage, quel que soit le réglage.", + ACT_TARGET = "Cibler", + ACT_MENU = "Menu d'unité", + ACT_MOVE = "Déplacer l'affichage", + ACT_NONE = "Rien", + LOSICON = "Ligne de vue", + LOSICON_TIP = "Affiche un œil dans le coin de la plaque tant que l'unité est hors de votre ligne de vue. Avec le mod client UnitXP, c'est vérifié en direct ; sans lui, l'addon mémorise une erreur « hors de la ligne de vue » pendant quelques secondes après une tentative de soin sur cette unité et l'efface dès qu'un lancement sur elle commence ou qu'un soin arrive.", + DEBUFFICON = "Icône de debuff", + DEBUFFICON_TIP = "Affiche l'icône du premier debuff que votre classe peut dissiper à côté du nom, avec son nombre de charges. La barre de vie prend aussi la couleur du debuff.", + MENU_NO_SPELL = "|cFF999999Aucun sort|r", + RANK_DEFAULT = "Par défaut", + PANEL_SUB = "Options de %s.\nChoisissez combien de boutons afficher\net quel sort chaque bouton lance.", + SHOW_BUTTONS = "Afficher |cFFFFFFFF%s|r boutons", + SCALE = "Échelle du cadre : |cFFFFFFFF%s", + SMALL = "Petit", + LARGE = "Grand", + BTN_SPACING = "Espacement des boutons : |cFFFFFFFF%s px", + ROW_SPACING = "Espacement des lignes : |cFFFFFFFF%s px", + ATTACH = "Cadres de groupe Blizzard", + ATTACH_TIP = "Attache les boutons de soins aux cadres de groupe par défaut de Blizzard au lieu d'utiliser les plaques déplaçables de l'addon.", + COMM = "Synchronisation HealComm", + COMM_TIP = "Diffuse vos soins au format HealComm pour que Puppeteer, pfUI, Luna et d'autres puissent les afficher, et intègre à votre propre prédiction les soins annoncés par les autres soigneurs.", + MANABAR = "Barre de mana", + MANABAR_TIP = "Affiche une fine barre de mana bleue le long du bord inférieur de la barre de vie, uniquement pour les unités qui utilisent du mana (ni rage, ni focalisation, ni énergie).", + SHOWPETS = "Afficher les familiers", + SHOWPETS_TIP = "Ajoute une plaque pour chaque familier du groupe (chasseur, démoniste) juste sous son maître, avec les mêmes boutons de soins.", + TESTMODE = "Mode test", + TESTMODE_TIP = "Remplit l'affichage de joueurs et de familiers fantômes pour tout organiser sans être en groupe. Les fantômes montrent la vie, le mana, un bouclier, des soins entrants et un debuff dissipable. Non sauvegardé, toujours désactivé à la connexion.", + TEST_ON = "Mode test |cFF00FF00activé|r, joueurs fantômes actifs. Les boutons ne lancent rien sur les fantômes.", + TEST_OFF = "Mode test |cFFFF0000désactivé|r.", + TEST_CLICK = "Mode test : aucun lancement sur un joueur fantôme.", + CLASSCOLORS = "Couleurs de classe", + CLASSCOLORS_TIP = "Colore le nom de chaque plaque avec la couleur de classe (style Healium). Les familiers gardent leur nom bleu clair.", + RANGEFADE = "Estompage hors portée", + RANGEFADE_TIP = "Rend toute la plaque, boutons compris, à moitié transparente quand l'unité est hors de portée de votre premier sort assigné (ou au-delà de 28 mètres si aucun sort n'est assigné).", + BUFFWATCH = "Surveillance de buff", + BUFFWATCH_TIP = "Choisissez l'un de vos buffs. Chaque plaque dont l'unité n'a pas ce buff reçoit une bordure orange. La version de groupe compte aussi (Prière de robustesse pour Mot de pouvoir : Robustesse, Don de la nature pour Marque du fauve, Bénédictions supérieures, ...).", + BUFFWATCH_NONE = "aucune", + BUFFWATCH_PETS = "Surveillance de buff : familiers", + BUFFWATCH_PETS_TIP = "Marque aussi les familiers auxquels le buff surveillé manque. Désactivé par défaut, car les familiers reçoivent rarement Robustesse ou des Bénédictions et leurs plaques resteraient orange la plupart du temps.", + MENU_NO_BUFF = "|cFF999999Aucune surveillance de buff|r", + LANG_TIP = "Change tous les textes de l'addon. Effet immédiat, sans rechargement.", + ABOUT = "%s %s |cFFAAAAAA(original de Dourd, UI Overhauled)|r\n|cFFAAAAAAPorté sur Vanilla et étendu en 09/2026 par Mquadrat|r", + MM_TIP = "Gauche : options\nMaintenir droit : déplacer ce bouton\nMaj + gauche : afficher/masquer l'affichage", + FBP_WATCHED = "Sorts lus dans le grimoire :", + FBP_ACTIVE = "Actifs en ce moment :", + FBP_DIRECT = "direct", + FBP_SHIELD = "bouclier", + FBP_LEARNED = "appris", + FBP_EVERY = "toutes les", + FBP_TICKSOF = "ticks de", + FBP_CAST = "Lancement", + FBP_ON = "sur", + FBP_OF = "sur", + FBP_FOR = "pour", + FBP_SYNC = "Synchronisation HealComm :", + FBP_STATE_ON = "activée", + FBP_STATE_OFF = "désactivée", + FBP_INCOMING = "Entrant", + FBP_DEBUG = "Débogage :", + FBP_RESET = "Valeurs apprises effacées.", + FBP_COMMANDS = "Commandes : /fbp config (fenêtre des options), /fbp test (mode test), /fbp buffs (diagnostic des buffs), /fbp debug, /fbp reset", + FBP_SMART = "Smart Healing : %s (marge de sécurité %d %%)", + DBG_HOT = "HoT %s sur %s : %d par tick, %ds", + DBG_SHIELD = "Bouclier %s sur %s : %d absorption, %ds", + DBG_CAST = "Lancement %s sur %s : %d", + DBG_TICK = "Tick corrigé : %s = %d", + DBG_HEAL = "Soin %s = %d (estimation maintenant %d)", + DBG_ABSORB = "Absorption %d sur %s (reste %d)", +}; + +FBLocale["itIT"] = { + LANG_NAME = "Italiano", + LANGUAGE = "Lingua", + LOADED = "caricato|r. Il pulsante della minimappa o /fbp config apre le opzioni, /fbp mostra la previsione delle cure.", + SUPERWOW = " |cFF55FF55[SuperWoW rilevato]|r", + CREDITS = "|cFFAAAAAAOriginale di Dourd (Argent Dawn EU), portato su Vanilla ed esteso nel 09/2026 da Mquadrat|r", + TT_NO_SPELL = "|cFFFFFFFFNessun incantesimo\n|cFF00FF00Scegli un incantesimo nella finestra delle opzioni.", + TT_TARGET = "Bersaglio", + NOT_IN_GROUP = " non è nel tuo gruppo.", + SELECT_SPELL = "Scegli incantesimo...", + BUTTON = "Pulsante", + TAB_BUTTONS = "Pulsanti", + TAB_GENERAL = "Generale", + COL_LEFT = "Clic sinistro", + COL_RIGHT = "Clic destro", + RIGHTCLICK = "Incantesimo del clic destro", + RIGHTCLICK_TIP = "Dà a ogni pulsante un secondo incantesimo con il clic destro (per es. Cura Rapida a sinistra, Cura Superiore a destra). Disattivato per impostazione predefinita. Quando è attivo compare una seconda colonna in alto e una piccola icona nell'angolo di ogni pulsante mostra l'incantesimo del clic destro. Rilascia un incantesimo con il tasto destro o tenendo premuto Maiusc per riempire questo lato.", + TT_RIGHT = "Clic destro", + TT_BUFF_UNKNOWN = "tempo rimanente sconosciuto (non è un tuo lancio)", + DROP_SET = "Pulsante %d: |cFFFFFFFF%s|r (trascinato dal libro degli incantesimi)", + DROP_SET_R = "Pulsante %d, clic destro: |cFFFFFFFF%s|r (trascinato dal libro degli incantesimi)", + DROP_UNKNOWN = "Impossibile identificare l'incantesimo trascinato.", + SMARTRANK = "Smart Healing", + SMARTRANK_TIP = "Cosa fa: al clic, invece del rango assegnato il pulsante lancia il rango più basso dello stesso incantesimo la cui cura prevista copre la salute mancante del bersaglio (meno le cure già in arrivo) più il margine di sicurezza. Risparmia mana e cure in eccesso. Regole: mai sopra il rango assegnato, solo cure dirette (HoT e scudi non sono toccati) e sempre il rango assegnato sotto il 30 % di salute. Svantaggi: la cura prevista è una stima dalla descrizione o dai valori appresi e ignora i critici; con danni a raffica, o quando vuoi curare in eccesso di proposito come cuscinetto (un tank prima di un colpo forte), il rango inferiore può non bastare. Disattivalo ogni volta che vuoi curare in eccesso. Ogni decisione è registrata con /fbp debug.", + SMART_MARGIN = "Margine di sicurezza: |cFFFFFFFF%s %%", + COOLDOWNS = "Recuperi sui pulsanti", + COOLDOWNS_TIP = "Mostra l'animazione del tempo di recupero su ogni pulsante (Rapidità della Natura, Concentrazione Interiore, Imposizione delle Mani, recupero dello scudo). Il tempo di recupero globale non viene mostrato.", + AGGRO = "Segnala chi è attaccato", + AGGRO_TIP = "Bordo rosso sulla targhetta o sulla cella del membro che il tuo bersaglio ostile sta puntando. Controllato cinque volte al secondo.", + BUFFICONS = "Icone benefici (sinistra)", + BUFFICONS_TIP = "I benefici con durata presenti sui tuoi pulsanti (Tempra, Spirito Divino, Protezione dalla Paura, ...) compaiono come piccole icone sul lato esterno sinistro della targhetta finché sono attivi. Come una lancetta dei minuti, l'icona diventa in bianco e nero in senso orario a partire dalle dodici man mano che il tempo scorre: tutta a colori all'inizio, metà destra grigia a metà tempo, tre quarti grigi quando ne resta un quarto. Esatta su te stesso, contata dal tuo lancio sugli altri; un beneficio lanciato da altri resta a colori (tempo sconosciuto). /fbp buffs mostra cosa viene seguito.", + TIMERS = "Timer HoT e scudo", + TIMERS_TIP = "Ogni pulsante mostra i secondi rimanenti del tuo HoT o scudo di quell'incantesimo su quell'unità: verde per gli HoT, blu per lo scudo, rosso per Anima Indebolita dopo lo scudo. I benefici con durata vengono mostrati come icone nella barra della salute (vedi Icone dei benefici).", + DBG_SMARTRANK = "Rango ridotto: %s -> %s (mancano %d, previsti %d)", + DROP_HINT = "Trascina gli incantesimi dal libro su un pulsante. Il tasto destro o Maiusc al rilascio riempie il lato del clic destro.", + STATE_DEAD = "Morto", + STATE_GHOST = "Fantasma", + STATE_OFFLINE = "Offline", + PLATE_LEFT = "Clic sinistro sulla targhetta", + PLATE_RIGHT = "Clic destro sulla targhetta", + PLATE_TIP = "Cosa fa un clic su una targhetta (nome o barra della salute). Maiusc + trascinamento con il sinistro sposta sempre la visualizzazione, qualunque sia l'impostazione.", + ACT_TARGET = "Seleziona", + ACT_MENU = "Menu unità", + ACT_MOVE = "Sposta la visualizzazione", + ACT_NONE = "Niente", + LOSICON = "Linea di vista", + LOSICON_TIP = "Mostra un occhio nell'angolo della targhetta finché l'unità è fuori dalla tua linea di vista. Con il mod client UnitXP il controllo è in tempo reale; senza, l'addon ricorda un errore di \"fuori linea di vista\" per qualche secondo dopo un tentativo di cura su quell'unità e lo cancella non appena inizia un lancio su di essa o arriva una cura.", + DEBUFFICON = "Icona del malus", + DEBUFFICON_TIP = "Mostra l'icona del primo malus che la tua classe può rimuovere accanto al nome, con il numero di accumuli. La barra della salute continua anche a prendere il colore del malus.", + MENU_NO_SPELL = "|cFF999999Nessun incantesimo|r", + RANK_DEFAULT = "Predefinito", + PANEL_SUB = "Opzioni di %s.\nScegli quanti pulsanti mostrare\ne quale incantesimo lancia ogni pulsante.", + SHOW_BUTTONS = "Mostra |cFFFFFFFF%s|r pulsanti", + SCALE = "Scala della cornice: |cFFFFFFFF%s", + SMALL = "Piccola", + LARGE = "Grande", + BTN_SPACING = "Spaziatura pulsanti: |cFFFFFFFF%s px", + ROW_SPACING = "Spaziatura righe: |cFFFFFFFF%s px", + ATTACH = "Cornici di gruppo Blizzard", + ATTACH_TIP = "Aggancia i pulsanti di cura alle cornici di gruppo predefinite di Blizzard invece di usare le targhette mobili dell'addon.", + COMM = "Sincronizzazione HealComm", + COMM_TIP = "Trasmette le tue cure nel formato HealComm perché Puppeteer, pfUI, Luna e altri possano mostrarle, e integra nella tua previsione le cure annunciate dagli altri guaritori.", + MANABAR = "Barra del mana", + MANABAR_TIP = "Mostra una sottile barra blu del mana lungo il bordo inferiore della barra della salute, solo per le unità che usano mana (niente ira, concentrazione o energia).", + SHOWPETS = "Mostra famigli", + SHOWPETS_TIP = "Aggiunge una targhetta per ogni famiglio del gruppo (cacciatore, stregone) subito sotto il suo padrone, con gli stessi pulsanti di cura.", + TESTMODE = "Modalità test", + TESTMODE_TIP = "Riempie la visualizzazione con giocatori e famigli fantasma per sistemare tutto senza essere in gruppo. I fantasmi mostrano salute, mana, uno scudo, cure in arrivo e un malus dissolvibile. Non viene salvata, sempre disattivata all'accesso.", + TEST_ON = "Modalità test |cFF00FF00attiva|r, giocatori fantasma attivi. I pulsanti non lanciano sui fantasmi.", + TEST_OFF = "Modalità test |cFFFF0000disattivata|r.", + TEST_CLICK = "Modalità test: nessun lancio su un giocatore fantasma.", + CLASSCOLORS = "Colori di classe", + CLASSCOLORS_TIP = "Colora il nome di ogni targhetta con il colore di classe (stile Healium). I famigli mantengono il nome azzurro.", + RANGEFADE = "Dissolvenza fuori portata", + RANGEFADE_TIP = "Rende l'intera targhetta, pulsanti compresi, semitrasparente quando l'unità è fuori portata del tuo primo incantesimo assegnato (o oltre 28 metri se nessun incantesimo è assegnato).", + BUFFWATCH = "Controllo beneficio", + BUFFWATCH_TIP = "Scegli uno dei tuoi benefici. Ogni targhetta la cui unità non ha quel beneficio riceve un bordo arancione. Conta anche la versione di gruppo (Preghiera della Tempra per Parola del Potere: Tempra, Dono della Natura per Marchio della Natura, Benedizioni Superiori, ...).", + BUFFWATCH_NONE = "nessuno", + BUFFWATCH_PETS = "Controllo beneficio: famigli", + BUFFWATCH_PETS_TIP = "Segnala anche i famigli a cui manca il beneficio controllato. Disattivato per impostazione predefinita, perché i famigli ricevono raramente Tempra o Benedizioni e le loro targhette resterebbero arancioni quasi sempre.", + MENU_NO_BUFF = "|cFF999999Nessun controllo beneficio|r", + LANG_TIP = "Cambia tutti i testi dell'addon. Effetto immediato, senza ricaricare.", + ABOUT = "%s %s |cFFAAAAAA(originale di Dourd, UI Overhauled)|r\n|cFFAAAAAAPortato su Vanilla ed esteso nel 09/2026 da Mquadrat|r", + MM_TIP = "Sinistro: opzioni\nTieni premuto il destro: sposta questo pulsante\nMaiusc + sinistro: mostra/nascondi la visualizzazione", + FBP_WATCHED = "Incantesimi letti dal libro:", + FBP_ACTIVE = "Attivi ora:", + FBP_DIRECT = "diretta", + FBP_SHIELD = "scudo", + FBP_LEARNED = "appreso", + FBP_EVERY = "ogni", + FBP_TICKSOF = "tick da", + FBP_CAST = "Lancio", + FBP_ON = "su", + FBP_OF = "di", + FBP_FOR = "per", + FBP_SYNC = "Sincronizzazione HealComm:", + FBP_STATE_ON = "attiva", + FBP_STATE_OFF = "disattivata", + FBP_INCOMING = "In arrivo", + FBP_DEBUG = "Debug:", + FBP_RESET = "Valori appresi scartati.", + FBP_COMMANDS = "Comandi: /fbp config (finestra opzioni), /fbp test (modalità test), /fbp buffs (diagnostica benefici), /fbp debug, /fbp reset", + FBP_SMART = "Smart Healing: %s (margine di sicurezza %d %%)", + DBG_HOT = "HoT %s su %s: %d per tick, %ds", + DBG_SHIELD = "Scudo %s su %s: %d assorbimento, %ds", + DBG_CAST = "Lancio %s su %s: %d", + DBG_TICK = "Tick corretto: %s = %d", + DBG_HEAL = "Cura %s = %d (stima ora %d)", + DBG_ABSORB = "Assorbimento %d su %s (restano %d)", +}; + FBL = FBLocale[FBDetectLocale()]; -- ========================================================================== @@ -366,16 +743,20 @@ FBPET_ICON_SIZE = 12; -- Debuff-Icon neben dem Namen FBDEBUFF_ICON_SIZE = 14; +-- Hoehe der Namensbox: genau eine Zeile (verhindert Umbruch und Verschieben) +FBNAME_HEIGHT = 12; + -- Breite der Namensbox: normal, und wenn rechts daneben das Debuff-Icon steht -- (rechts davon braucht der Prozenttext etwa 34 px) FBNAME_WIDTH_FULL = NamePlateWidth - 42; -- 78 FBNAME_WIDTH_ICON = NamePlateWidth - 42 - FBDEBUFF_ICON_SIZE - 4; -- 60 --- Sichtlinien-Abzeichen: Icon, Groesse, Position (Anker RIGHT an LEFT der Plakette) +-- Sichtlinien-Abzeichen: Icon, Groesse, Position (Anker TOPLEFT an TOPLEFT +-- der Plakette, ragt etwas ueber die Ecke; links aussen sitzen die Buff-Icons) FBLOS_ICON = "Interface\\Icons\\Spell_Shadow_MindSteal"; -- das "blinde Auge" (Blenden) -FBLOS_ICON_SIZE = 16; -FBLOS_ICON_X = 6; -- wie weit das Abzeichen in die Plakette hineinragt -FBLOS_ICON_Y = 0; +FBLOS_ICON_SIZE = 12; +FBLOS_ICON_X = -3; +FBLOS_ICON_Y = 3; FBLOS_TIMEOUT = 8; -- Sek., wie lange eine LoS-Fehlermeldung ohne UnitXP gilt -- Reichweiten-Fading: Deckkraft einer Plakette ausser Reichweite, Pruefintervall @@ -385,6 +766,31 @@ FBRANGE_INTERVAL = 0.5; -- Sekunden -- Rahmenfarbe, wenn der ueberwachte Buff fehlt (Buff-Wache) FBBUFF_MISSING_COLOR = { 1.0, 0.5, 0.0, 1 }; FBBUFF_NORMAL_COLOR = { 1.0, 1.0, 1.0, 1 }; +-- Rahmenfarbe fuer den Angegriffenen (schlaegt die Buff-Wache) +FBAGGRO_COLOR = { 1.0, 0.15, 0.15, 1 }; + +-- Cooldown-Uhr: kuerzere Cooldowns (globaler Cooldown) werden nicht gezeigt +FBCD_MIN_DURATION = 2.0; + +-- Buff-Icons mit Uhr: 8 px, aussen links neben der Plakette (und Zelle), +-- vertikal mittig, von rechts nach links aufgereiht. Die Uhr: vier +-- Quadranten werden im Uhrzeigersinn ab 12 Uhr schwarz-weiss, wie ein +-- Minutenzeiger, der ueber das Icon laeuft (50 % Rest = rechte Haelfte grau). +FBBUFFICON_SIZE = 8; +FBBUFFICON_GAP = 1; +FBBUFFICON_MAX = 6; +FBBUFFICON_XOFF = -2; -- px Abstand des ersten Icons zur linken Plattenkante +FBBUFFICON_GREY = 0.30; -- Grauton der abgelaufenen Quadranten (falls keine Entsaettigung) +FBBUFFICON_WASH = { 0, 0, 0, 0.45 }; -- dunkle Waesche ueber abgelaufenen Quadranten +FBBUFFICON_ROWS = 2; -- Icons werden in Zweierstapeln (2 hoch) nach links aufgereiht + +-- HoT-/Schild-Timer auf den Buttons +FBTIMER_COLOR_HOT = { 0.4, 1.0, 0.4 }; +FBTIMER_COLOR_SHIELD = { 0.6, 0.8, 1.0 }; +FBTIMER_COLOR_WS = { 1.0, 0.3, 0.3 }; +FBTIMER_COLOR_BUFF = { 1.0, 0.9, 0.4 }; +FBWEAKENED_SOUL_SEC = 15; +FBSHIELD_SPELL = "Power Word: Shield"; -- Klassenfarben: RAID_CLASS_COLORS des Clients, sonst diese Vorgaben FBClassColors = { @@ -469,17 +875,28 @@ FBTestMode = false; -- hp/mp sind Anteile (0..1), swing ist die Amplitude der HP-Schwankung, -- debuff = true gibt dem Geist einen von der eigenen Klasse entfernbaren Debuff --- (Icon debuffTex, debuffCount Stacks), +-- (Icon debuffTex, debuffCount Stacks), debuffType einen bestimmten Typ +-- (z. B. "Disease"; faerbt nur, wenn die eigene Klasse ihn entfernen kann), +-- state = "dead" | "ghost" | "offline", -- buffMissing = true laesst die Buff-Wache anschlagen, outOfRange = true faded, --- los = true zeigt das Sichtlinien-Abzeichen. +-- los = true zeigt das Sichtlinien-Abzeichen, aggro = true den roten Rahmen, +-- hotLeft/shieldLeft Restsekunden auf Button 1 bzw. 2. FBTestGhosts = { - ["party1"] = { name = "Brynn", class = "WARRIOR", hpMax = 3400, hp = 0.90, swing = 0.08, hasMana = false, buffMissing = true }, + ["party1"] = { name = "Brynn", class = "WARRIOR", hpMax = 3400, hp = 0.90, swing = 0.08, hasMana = false, buffMissing = true, state = "dead" }, ["party2"] = { name = "Cerys", class = "WARLOCK", hpMax = 2300, hp = 0.55, swing = 0.10, hasMana = true, mpMax = 3100, mp = 0.65, shield = 450, los = true }, - ["party3"] = { name = "Dorn", class = "HUNTER", hpMax = 2900, hp = 0.30, swing = 0.15, hasMana = true, mpMax = 2400, mp = 0.35, inc = 700 }, + ["party3"] = { name = "Dorn", class = "HUNTER", hpMax = 2900, hp = 0.30, swing = 0.15, hasMana = true, mpMax = 2400, mp = 0.35, inc = 700, aggro = true, hotLeft = 9, shieldLeft = 21, + -- sechs Buffs: Seelenstaerke, Willen, Schattenschutz, Furchtzauberschutz, Mal der Wildnis (fremd, ohne Uhr), Koenige + buffs = { { tex = "Interface\\Icons\\Spell_Holy_WordFortitude", left = 540, dur = 1800 }, + { tex = "Interface\\Icons\\Spell_Holy_DivineSpirit", left = 1500, dur = 1800 }, + { tex = "Interface\\Icons\\Spell_Shadow_AntiShadow", left = 300, dur = 600 }, + { tex = "Interface\\Icons\\Spell_Holy_Excorcism", left = 45, dur = 180 }, + { tex = "Interface\\Icons\\Spell_Nature_Regeneration", dur = 1800 }, + { tex = "Interface\\Icons\\Spell_Magic_MageArmor", left = 1750, dur = 1800 } } }, ["party4"] = { name = "Elowen", class = "DRUID", hpMax = 2700, hp = 0.95, swing = 0.04, hasMana = true, mpMax = 3600, mp = 0.90, debuff = true, debuffTex = "Interface\\Icons\\Spell_Shadow_ShadowWordPain", debuffCount = 3, outOfRange = true }, ["pet"] = { name = "Fang", hpMax = 1900, hp = 0.75, swing = 0.12, hasMana = false }, ["partypet2"] = { name = "Zorbek", hpMax = 1200, hp = 0.60, swing = 0.10, hasMana = true, mpMax = 900, mp = 0.50 }, - ["partypet3"] = { name = "Bramble", hpMax = 2100, hp = 0.40, swing = 0.14, hasMana = false, inc = 300 }, + ["partypet3"] = { name = "Bramble", hpMax = 2100, hp = 0.40, swing = 0.14, hasMana = false, inc = 300, + debuffType = "Disease", debuffTex = "Interface\\Icons\\Spell_Nature_NullifyDisease", debuffCount = 1 }, }; function FBTest_Ghost(unit) @@ -705,6 +1122,12 @@ function FBHealBox_ApplyDefaults() if (HealBox.DebuffIcon == nil) then HealBox.DebuffIcon = 1; end if (HealBox.LOSIcon == nil) then HealBox.LOSIcon = 1; end if (HealBox.BuffWatchPets == nil) then HealBox.BuffWatchPets = 0; end + if (HealBox.SmartRank == nil) then HealBox.SmartRank = 0; end + if (HealBox.SmartMargin == nil) then HealBox.SmartMargin = 20; end + if (HealBox.Cooldowns == nil) then HealBox.Cooldowns = 1; end + if (HealBox.AggroMark == nil) then HealBox.AggroMark = 1; end + if (HealBox.SpellTimers == nil) then HealBox.SpellTimers = 1; end + if (HealBox.BuffIcons == nil) then HealBox.BuffIcons = 1; end if (not FBPlateActionName[HealBox.PlateLeft or ""]) then HealBox.PlateLeft = "target"; end if (not FBPlateActionName[HealBox.PlateRight or ""]) then HealBox.PlateRight = "target"; end FBHealBox_RunHook("Defaults"); @@ -727,6 +1150,12 @@ function FBHealBox_SyncOptions() if (LOSIconCheck) then LOSIconCheck:SetChecked(HealBox.LOSIcon == 1); end if (BuffWatchPetsCheck) then BuffWatchPetsCheck:SetChecked(HealBox.BuffWatchPets == 1); end if (RightClickCheck) then RightClickCheck:SetChecked(HealBox.RightClick == 1); end + if (SmartRankCheck) then SmartRankCheck:SetChecked(HealBox.SmartRank == 1); end + if (SmartMarginSlider) then SmartMarginSlider:SetValue(HealBox.SmartMargin); end + if (CooldownsCheck) then CooldownsCheck:SetChecked(HealBox.Cooldowns == 1); end + if (AggroMarkCheck) then AggroMarkCheck:SetChecked(HealBox.AggroMark == 1); end + if (SpellTimersCheck) then SpellTimersCheck:SetChecked(HealBox.SpellTimers == 1); end + if (BuffIconsCheck) then BuffIconsCheck:SetChecked(HealBox.BuffIcons == 1); end FBHealBox_UpdateBuffWatchLabel(); FBHealBox_UpdatePlateActionLabels(); FBHealBox_ApplyRightClickLayout(); @@ -789,6 +1218,8 @@ function FBLoadSpellData() FBPredict_BuildWatch(); -- Doppelt gezaehlte Absorb-Lernwerte (Versionen vor 1.4.2) verwerfen FBPredict_SanitizeMemory(); + -- Watch ist neu: vorhandene Buffs sofort einlesen + FBPredict_ScanAllUnits(); if (not HealBox.SpellChoiceR) then HealBox.SpellChoiceR = {}; end for _, side in ipairs({ "L", "R" }) do @@ -1137,7 +1568,7 @@ function FBMenu_OpenLanguageMenu(anchorFrame) end local entries = {}; - local codes = { "deDE", "enUS" }; + local codes = { "deDE", "enUS", "esES", "frFR", "itIT" }; for _, code in ipairs(codes) do local e = {}; e.text = FBLocale[code].LANG_NAME; @@ -1378,6 +1809,7 @@ function FBHealBox_SetPlateVisible(f, visible) if (f.isPet and f.PetIcon) then f.PetIcon:Hide(); end f.HealthBar:Hide(); f.ShieldBar:Hide(); f.IncHealBar:Hide(); f.ManaBar:Hide(); f.DebuffIcon:Hide(); f.DebuffCount:Hide(); f.LOSIcon:Hide(); + if (f.buffIcons) then for _, ic in ipairs(f.buffIcons) do ic:Hide(); end end end end @@ -1497,7 +1929,7 @@ function FBHealBoxCreateFrame(FrameName,ParentFrame,FrameTexture,FrameWidth,Fram f.PetIcon = f:CreateTexture(nil, "OVERLAY"); f.PetIcon:SetWidth(FBPET_ICON_SIZE); f.PetIcon:SetHeight(FBPET_ICON_SIZE); - f.PetIcon:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -7); + f.PetIcon:SetPoint("LEFT", f, "LEFT", 4, 0); f.PetIcon:SetTexture(FBPET_ICON); f.PetIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93); end @@ -1505,8 +1937,13 @@ function FBHealBoxCreateFrame(FrameName,ParentFrame,FrameTexture,FrameWidth,Fram -- Name: linksbuendig, volle Breite. Nur solange ein Debuff-Icon zu sehen -- ist, wird die Box schmaler (FBHealBox_UpdateDebuffIcon). f.NameText = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); - f.NameText:SetPoint("TOPLEFT", f, "TOPLEFT", nameX, -8); + -- Fest vertikal zentriert mit Einzeilen-Hoehe: so kann ein internes + -- Umbrechen des Textfelds den Namen nicht nach unten schieben. + f.NameText:SetPoint("LEFT", f, "LEFT", nameX, 0); + f.nameX = nameX; + f.NameText:SetHeight(FBNAME_HEIGHT); f.NameText:SetJustifyH("LEFT"); + f.NameText:SetJustifyV("MIDDLE"); f.NameText:SetText(""); if (isPet) then f.NameText:SetTextColor(FBPET_NAME_COLOR[1], FBPET_NAME_COLOR[2], FBPET_NAME_COLOR[3], FBPET_NAME_COLOR[4]); @@ -1519,7 +1956,7 @@ function FBHealBoxCreateFrame(FrameName,ParentFrame,FrameTexture,FrameWidth,Fram f.DebuffIcon = f:CreateTexture(nil, "OVERLAY"); f.DebuffIcon:SetWidth(FBDEBUFF_ICON_SIZE); f.DebuffIcon:SetHeight(FBDEBUFF_ICON_SIZE); - f.DebuffIcon:SetPoint("TOPLEFT", f, "TOPLEFT", nameX + f.nameWidthIcon + 3, -7); + f.DebuffIcon:SetPoint("LEFT", f, "LEFT", nameX + f.nameWidthIcon + 3, 0); f.DebuffIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93); f.DebuffIcon:Hide(); f.DebuffCount = f:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall"); @@ -1531,7 +1968,7 @@ function FBHealBoxCreateFrame(FrameName,ParentFrame,FrameTexture,FrameWidth,Fram f.LOSIcon = f:CreateTexture(nil, "OVERLAY"); f.LOSIcon:SetWidth(FBLOS_ICON_SIZE); f.LOSIcon:SetHeight(FBLOS_ICON_SIZE); - f.LOSIcon:SetPoint("RIGHT", f, "LEFT", FBLOS_ICON_X, FBLOS_ICON_Y); + f.LOSIcon:SetPoint("TOPLEFT", f, "TOPLEFT", FBLOS_ICON_X, FBLOS_ICON_Y); f.LOSIcon:SetTexture(FBLOS_ICON); f.LOSIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93); f.LOSIcon:Hide(); @@ -1685,6 +2122,59 @@ function FBHealBox_DropSpell(btnIndex, side) return true; end +-- ========================================================================== +-- [ Automatisches Abrangen ] +-- +-- Niedrigsten Rang des Zaubers waehlen, dessen erwartete Heilung das +-- fehlende Leben des Ziels (abzueglich schon eingehender Heilung) plus +-- Sicherheitsaufschlag deckt. Nie ueber dem belegten Rang, nur fuer +-- Direktheilungen, im Notfall (unter VeryLowHP) immer der belegte Rang. +-- Erwartete Heilung: gelernter Wert, sonst Tooltip-Mittelwert. +-- ========================================================================== + +function FBHealBox_SmartRank(castString, unit) + if (HealBox.SmartRank ~= 1) or (not castString) then return castString; end + local base, rank = FBPredict_SplitCast(castString); + if (not rank) then return castString; end + local ranks = FBPlayerSpells[base]; + if (not ranks) or (table.getn(ranks) < 2) then return castString; end + if (not FBUnitExists(unit)) or FBTest_Ghost(unit) then return castString; end + + local hp, hpMax = FBUnitHealth(unit); + if (hpMax <= 0) or ((hp / hpMax) <= VeryLowHP) then return castString; end + local name = FBUnitName(unit); + local incoming = FBGetDirectHeal(name) + FBGetHoTHeal(name) + FBGetCommHeal(name); + local deficit = hpMax - hp - incoming; + if (deficit < 0) then deficit = 0; end + local need = deficit * (1 + (HealBox.SmartMargin or 20) / 100); + + local assignedIdx = nil; + for i, sd in ipairs(ranks) do + if (sd.rank == rank) then assignedIdx = i; end + end + if (not assignedIdx) then return castString; end + + for i = 1, assignedIdx do + local sd = ranks[i]; + local info = FBPredict_GetSpellInfo(sd.id, base); + -- nur echte Direktheilungen: Heilbetrag vorhanden, "heal" im Tooltip, + -- kein Schild, kein Buff. Alles andere geht im belegten Rang raus. + if (not info) or (not info.direct) or info.shield or (not info.isHeal) then return castString; end + local amount = FBPredict_Remembered("direct", base, sd.rank) or info.direct; + if (amount >= need) then + if (i == assignedIdx) then return castString; end + local chosen = base; + if (sd.rank and sd.rank ~= "") then chosen = base.."("..sd.rank..")"; end + if (FBPredictDebug) then + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..format(FBT("DBG_SMARTRANK"), + castString, chosen, math.floor(deficit), math.floor(amount))); + end + return chosen; + end + end + return castString; +end + -- Zauber castString auf das Ziel des Buttons wirken (Links- oder Rechtsklick) function FBHealBox_CastOn(button, castString) if (not castString) then return; end @@ -1697,6 +2187,7 @@ function FBHealBox_CastOn(button, castString) end if (castTarget == "player") then + castString = FBHealBox_SmartRank(castString, "player"); -- Ziel VOR dem Cast merken: SPELLCAST_START feuert sofort FBPredict_NoteCast(castString, UnitName("player")); CastSpellByName(castString, 1); @@ -1708,6 +2199,8 @@ function FBHealBox_CastOn(button, castString) return; end + castString = FBHealBox_SmartRank(castString, castTarget); + if (SUPERWOW_VERSION or SUPERWOW_STRING) then FBPredict_NoteCast(castString, UnitName(castTarget)); CastSpellByName(castString, castTarget); @@ -1737,6 +2230,16 @@ function FBHealBoxCreateButton(FBButtonName, FBParentFrame, xoffset, yoffset, te button.icon = button:CreateTexture(nil, "BACKGROUND"); button.icon:SetAllPoints(); + -- Cooldown-Uhr (Blizzards Modell, wie auf den Aktionsleisten) + button.cooldown = CreateFrame("Model", nil, button, "CooldownFrameTemplate"); + button.cooldown:SetAllPoints(button); + button.cooldown:Hide(); + + -- HoT-/Schild-Restzeit + button.timer = button:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall"); + button.timer:SetPoint("TOP", button, "TOP", 0, -1); + button.timer:Hide(); + -- Eck-Icon fuer den Rechtsklick-Zauber (nur sichtbar, wenn belegt und aktiviert) button.subIcon = button:CreateTexture(nil, "OVERLAY"); button.subIcon:SetWidth(12); @@ -1813,6 +2316,7 @@ function FBHealBoxCreateButton(FBButtonName, FBParentFrame, xoffset, yoffset, te end); button:RegisterEvent("SPELL_UPDATE_USABLE"); + button:RegisterEvent("SPELL_UPDATE_COOLDOWN"); button:SetScript("OnEvent", HealBoxButton_OnEvent); return button; end @@ -1870,6 +2374,16 @@ function FBHealBox_Layout() end end +-- Alle vorhandenen Einheiten einmal aktiv nach Buffs scannen: nach /reload +-- oder Gruppenwechsel kommt sonst kein UNIT_AURA, bis sich etwas aendert. +function FBPredict_ScanAllUnits() + if (not FBPredict_ScanUnit) or FBTestMode then return; end + for p = 1, FBSlotCount do + local unit = FBPartyUnit[p]; + if (UnitExists(unit)) then FBPredict_ScanUnit(unit); end + end +end + function FBUpdateNames() if (not FBHealBox1) then return; end for p = 1, FBSlotCount do FBPartyFrame[p]:Hide(); end @@ -1888,6 +2402,7 @@ function FBUpdateNames() end FBHealBox_Layout(); + FBPredict_ScanAllUnits(); FBHealBox_CheckAllWatchBuffs(); FBHealBox_CheckRangeAll(); FBHealBox_CheckLOSAll(); @@ -2008,12 +2523,172 @@ function FBHealBox_CheckWatchBuff(unit, f) end if (f.buffMissing ~= missing) then f.buffMissing = missing; - local c = FBBUFF_NORMAL_COLOR; - if (missing) then c = FBBUFF_MISSING_COLOR; end - f:SetBackdropBorderColor(c[1], c[2], c[3], c[4]); + FBHealBox_ApplyBorder(f); end end +-- Rahmenfarbe einer Plakette: Angegriffener (rot) vor Buff-Wache (orange) vor normal +function FBHealBox_ApplyBorder(f) + if (not f) or (not f.SetBackdropBorderColor) then return; end + local c = FBBUFF_NORMAL_COLOR; + if (f.buffMissing) then c = FBBUFF_MISSING_COLOR; end + if (f.underAttack) then c = FBAGGRO_COLOR; end + f:SetBackdropBorderColor(c[1], c[2], c[3], c[4]); +end + +-- ========================================================================== +-- [ Wer wird angegriffen ] +-- +-- Hat dein Ziel ein feindliches Ziel im Ziel, bekommt dessen Plakette einen +-- roten Rahmen. Laeuft im 0.2-s-Takt der Vorhersage. Module haengen sich +-- ueber den Hook "Aggro" an (Raid-Zellen). +-- ========================================================================== + +-- Unit-ID des Angegriffenen ("targettarget") oder nil +function FBHealBox_AggroUnit() + if (HealBox.AggroMark ~= 1) then return nil; end + if (not UnitExists("target")) or (not UnitExists("targettarget")) then return nil; end + if (UnitCanAttack and not UnitCanAttack("player", "target")) then return nil; end + return "targettarget"; +end + +function FBHealBox_UnitIsAggro(unit, tt) + if (not tt) or (not unit) then return false; end + if (UnitIsUnit) then return (UnitIsUnit(tt, unit) == 1) or (UnitIsUnit(tt, unit) == true); end + return (UnitName(tt) == UnitName(unit)); +end + +function FBHealBox_CheckAggroAll() + local tt = FBHealBox_AggroUnit(); + for p = 1, FBSlotCount do + local f = FBPartyFrame[p]; + if (f) then + local flag = false; + local unit = FBPartyUnit[p]; + local g = FBTest_Ghost(unit); + if (HealBox.AggroMark == 1 and f:IsShown()) then + if (g) then flag = (g.aggro == true); + elseif (tt and FBUnitExists(unit)) then flag = FBHealBox_UnitIsAggro(unit, tt); end + end + if (f.underAttack ~= flag) then + f.underAttack = flag; + FBHealBox_ApplyBorder(f); + end + end + end + FBHealBox_RunHook("Aggro", tt); +end + +-- ========================================================================== +-- [ HoT- und Schild-Timer auf den Buttons ] +-- +-- Der Button eines Zaubers zeigt fuer seine Einheit die Restsekunden des +-- eigenen HoTs bzw. Schilds dieses Zaubers; nach dem Schild die Geschwaechte +-- Seele (15 s ab Anlegen) in Rot. Nur eigene Effekte, weil nur die verfolgt +-- werden. Module: Hook "SpellTimers". +-- ========================================================================== + +-- Text und Farbe fuer einen Button, oder nil +function FBHealBox_SpellTimerFor(castString, unitName, now, g, btnIndex) + if (g) then + if (btnIndex == 1 and g.hotLeft) then return tostring(g.hotLeft), FBTIMER_COLOR_HOT; end + if (btnIndex == 2 and g.shieldLeft) then return tostring(g.shieldLeft), FBTIMER_COLOR_SHIELD; end + return nil; + end + if (not castString) or (not unitName) then return nil; end + local base = FBPredict_SplitCast(castString); + 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; +end + +function FBHealBox_FormatTimer(secs) + if (secs >= 60) then return math.ceil(secs / 60) .. "m"; end + return tostring(math.ceil(secs)); +end + +function FBHealBox_SetButtonTimer(b, text, color) + if (not b) or (not b.timer) then return; end + if (text) then + b.timer:SetText(text); + b.timer:SetTextColor(color[1], color[2], color[3], 1); + b.timer:Show(); + else + b.timer:Hide(); + end +end + +function FBHealBox_UpdateSpellTimers() + local now = GetTime(); + local on = (HealBox.SpellTimers == 1); + for p = 1, FBSlotCount do + local f = FBPartyFrame[p]; + local unit = FBPartyUnit[p]; + if (f and FBPartyTable[p]) then + local shown = on and f:IsShown() and FBUnitExists(unit); + local name = shown and FBUnitName(unit); + local g = shown and FBTest_Ghost(unit); + for i = 1, MaxButtonCount do + local b = FBPartyTable[p][i]; + if (b) then + if (shown and b:IsShown()) then + local text, color = FBHealBox_SpellTimerFor(b.spellName, name, now, g, i); + if (not text and b.spellNameR) then + text, color = FBHealBox_SpellTimerFor(b.spellNameR, name, now, nil, i); + end + FBHealBox_SetButtonTimer(b, text, color); + else + FBHealBox_SetButtonTimer(b, nil); + end + end + end + end + end + FBHealBox_RunHook("SpellTimers", now); +end + +-- ========================================================================== +-- [ Cooldown-Uhr ] +-- ========================================================================== + +function FBHealBox_UpdateButtonCooldown(b) + if (not b) or (not b.cooldown) or (not CooldownFrame_SetTimer) then return; end + if (HealBox.Cooldowns ~= 1) or (not b.id) then + CooldownFrame_SetTimer(b.cooldown, 0, 0, 0); + return; + end + local start, duration, enable = GetSpellCooldown(b.id, BOOKTYPE_SPELL); + if (start and duration and duration > FBCD_MIN_DURATION) then + CooldownFrame_SetTimer(b.cooldown, start, duration, enable); + else + CooldownFrame_SetTimer(b.cooldown, 0, 0, 0); + end +end + +function FBHealBox_UpdateAllCooldowns() + for p = 1, FBSlotCount do + for i = 1, MaxButtonCount do + local b = FBPartyTable[p] and FBPartyTable[p][i]; + if (b) then FBHealBox_UpdateButtonCooldown(b); end + end + end + FBHealBox_RunHook("Cooldowns"); +end + function FBHealBox_CheckAllWatchBuffs() for p = 1, FBSlotCount do if (FBPartyFrame[p]) then FBHealBox_CheckWatchBuff(FBPartyUnit[p], FBPartyFrame[p]); end @@ -2230,6 +2905,27 @@ function FBHealBox_ApplyLocale() RightClickCheck.Text:SetText(FBT("RIGHTCLICK")); RightClickCheck.tooltipText = FBT("RIGHTCLICK_TIP"); end + if (SmartRankCheck) then + SmartRankCheck.Text:SetText(FBT("SMARTRANK")); + SmartRankCheck.tooltipText = FBT("SMARTRANK_TIP"); + end + FBUpdateSmartMarginText(); + if (CooldownsCheck) then + CooldownsCheck.Text:SetText(FBT("COOLDOWNS")); + CooldownsCheck.tooltipText = FBT("COOLDOWNS_TIP"); + end + if (AggroMarkCheck) then + AggroMarkCheck.Text:SetText(FBT("AGGRO")); + AggroMarkCheck.tooltipText = FBT("AGGRO_TIP"); + end + if (SpellTimersCheck) then + SpellTimersCheck.Text:SetText(FBT("TIMERS")); + SpellTimersCheck.tooltipText = FBT("TIMERS_TIP"); + end + if (BuffIconsCheck) then + BuffIconsCheck.Text:SetText(FBT("BUFFICONS")); + BuffIconsCheck.tooltipText = FBT("BUFFICONS_TIP"); + end FBHealBox_UpdateBuffWatchLabel(); FBHealBox_UpdatePlateActionLabels(); if (FBLangBtn) then @@ -2246,6 +2942,9 @@ end -- Steuerelemente beziehen sich damit weiterhin auf die linke obere Ecke. FBOPT_TAB_Y = -92; -- Hoehe der Reiterleiste +FBOPT_TAB_W = 100; -- Breite eines Reiterknopfs +FBOPT_CHECK_TEXT_W = 170; -- Breite der Schalterbeschriftungen (zwei Spalten je Reiter) +FBOPT_TAB_PITCH = 104; -- Abstand von Knopf zu Knopf (vier passen nebeneinander) FBOPT_CONTENT_Y = -128; -- erste Inhaltszeile unter der Reiterleiste -- Auswahlknopf mit Icon und Text (Zauberfelder, Sprache, Buff-Wache) @@ -2284,7 +2983,7 @@ end -- Reiter-Knopf function FBHealBox_CreateTabButton(name, parent, x, index) local t = CreateFrame("Button", name, parent); - t:SetWidth(110); + t:SetWidth(FBOPT_TAB_W); t:SetHeight(24); t:SetPoint("TOPLEFT", parent, "TOPLEFT", x, FBOPT_TAB_Y); t:SetBackdrop({ @@ -2313,12 +3012,22 @@ function FBHealBox_AddOptionsTab(labelKey) tab:Hide(); panel.tabs[i] = tab; panel.tabLabelKeys[i] = labelKey; - panel.tabButtons[i] = FBHealBox_CreateTabButton("HealBoxOptionsTabButton"..i, panel, 25 + (i - 1) * 116, i); + panel.tabButtons[i] = FBHealBox_CreateTabButton("HealBoxOptionsTabButton"..i, panel, 25 + (i - 1) * FBOPT_TAB_PITCH, i); panel.tabButtons[i].text:SetText(FBT(labelKey)); if (panel.activeTab) then FBHealBox_ShowTab(panel.activeTab); end return tab; end +-- Inhalts-Frame eines vorhandenen Reiters (fuer Module, die sich einen +-- Reiter teilen). nil, wenn es ihn nicht gibt. +function FBHealBox_FindOptionsTab(labelKey) + if (not panel) or (not panel.tabs) then return nil; end + for i, key in ipairs(panel.tabLabelKeys) do + if (key == labelKey) then return panel.tabs[i]; end + end + return nil; +end + function FBHealBox_ShowTab(index) if (not panel) or (not panel.tabs) then return; end FBMenu_CloseAll(); @@ -2348,7 +3057,6 @@ function FBHealBox_ApplyRightClickLayout() if (on) then FBSpellBtnsR[i]:Show(); else FBSpellBtnsR[i]:Hide(); end end end - if (on) then panel.colRight:Show(); else panel.colRight:Hide(); end end function FBHealBoxCreateAddonOptionFrame() @@ -2396,16 +3104,18 @@ function FBHealBoxCreateAddonOptionFrame() panel.TitleSubText:SetText(format(FBT("PANEL_SUB"), FBADDON_NAME)); panel.TitleSubText:SetTextColor(1, 1, 1, 1); + -- Klassen-Icon samt Name muss oberhalb der Reiterleiste (FBOPT_TAB_Y) + -- bleiben: Icon 52 px, Name direkt darunter, Unterkante bei etwa -84 local classIcon = CreateFrame("Frame", nil, panel); - classIcon:SetPoint("TOPRIGHT", -25, -20); - classIcon:SetWidth(70); - classIcon:SetHeight(70); + classIcon:SetPoint("TOPRIGHT", -25, -16); + classIcon:SetWidth(52); + classIcon:SetHeight(52); classIcon.tex = classIcon:CreateTexture(nil, "BACKGROUND"); classIcon.tex:SetAllPoints(); classIcon.tex:SetTexture(ClassIcon[FBClass]); - classIcon.text = classIcon:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge"); + classIcon.text = classIcon:CreateFontString(nil, "OVERLAY", "GameFontNormal"); classIcon.text:SetText(strupper(FBClass)); - classIcon.text:SetPoint("CENTER", 0, -45); + classIcon.text:SetPoint("TOP", classIcon, "BOTTOM", 0, -2); classIcon.text:SetTextColor(1, 1, 0.2, 1); -- [ Reiter ] ------------------------------------------------------------ @@ -2424,27 +3134,68 @@ function FBHealBoxCreateAddonOptionFrame() -- ====================================================================== -- Reiter 1: Button-Belegung - -- Zeile je Button: Label | Linksklick-Feld | Rechtsklick-Feld (optional) + -- Oben Smart Healing und Sicherheitsaufschlag, dann Spaltenkoepfe, + -- je Button eine Zeile: Label | Linksklick-Feld | Rechtsklick-Feld, + -- darunter Buttonzahl und Rechtsklick-Schalter. -- ====================================================================== - local rowH = 32; + local rowH = 30; -- Zeilenabstand (Felder 28 px hoch, 2 px Luft) + local fieldH = 28; local xLabel = 30; local xLeft = 92; local xRight = 268; local fieldW = 170; - local y0 = FBOPT_CONTENT_Y - 18; + + -- Smart Healing mit Sicherheitsaufschlag (oben) + SmartRankCheck = FBHealBox_CreateCheck("FBHealBoxSmartRankCheck", tabButtons, 34, FBOPT_CONTENT_Y, "SMARTRANK", "SMARTRANK_TIP", function() + HealBox.SmartRank = SmartRankCheck:GetChecked() and 1 or 0; + end); + SmartRankCheck:SetChecked(nil); + + SmartMarginSlider = CreateFrame("Slider", "FBSmartMarginSlider", tabButtons, "OptionsSliderTemplate"); + SmartMarginSlider:SetWidth(fieldW); + SmartMarginSlider:SetHeight(16); + SmartMarginSlider:SetPoint("TOPLEFT", xRight, FBOPT_CONTENT_Y - 20); + SmartMarginSlider:SetMinMaxValues(0, 50); + SmartMarginSlider:SetValueStep(5); + SmartMarginSlider:SetValue(HealBox.SmartMargin or 20); + SmartMarginSlider.Text = SmartMarginSlider:CreateFontString(nil, "BACKGROUND", "GameFontNormal"); + SmartMarginSlider.Text:SetPoint("CENTER", 0, 15); + getglobal(SmartMarginSlider:GetName() .. "Low"):SetText("0"); + getglobal(SmartMarginSlider:GetName() .. "High"):SetText("50"); + SmartMarginSlider:SetScript("OnValueChanged", function() + HealBox.SmartMargin = math.floor(SmartMarginSlider:GetValue() + 0.5); + FBUpdateSmartMarginText(); + end); + FBUpdateSmartMarginText(); + + -- Spaltenkoepfe und Zeilen + local yHead = FBOPT_CONTENT_Y - 52; + local y0 = yHead - 16; panel.colLeft = tabButtons:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); - panel.colLeft:SetPoint("TOPLEFT", tabButtons, "TOPLEFT", xLeft + 4, FBOPT_CONTENT_Y); + panel.colLeft:SetPoint("TOPLEFT", tabButtons, "TOPLEFT", xLeft + 4, yHead); panel.colLeft:SetText(FBT("COL_LEFT")); + -- Der Spaltenkopf der rechten Spalte ist der Schalter selbst (unten); + -- dieser Text bleibt als Platzhalter verborgen. panel.colRight = tabButtons:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); - panel.colRight:SetPoint("TOPLEFT", tabButtons, "TOPLEFT", xRight + 4, FBOPT_CONTENT_Y); + panel.colRight:SetPoint("TOPLEFT", tabButtons, "TOPLEFT", xRight + 4, yHead); panel.colRight:SetText(FBT("COL_RIGHT")); + panel.colRight:Hide(); + + -- Rechtsklick-Zweitzauber: Schalter als Kopf der rechten Spalte, + -- bewusst nur hier einschaltbar, Standard aus + RightClickCheck = FBHealBox_CreateCheck("FBHealBoxRightClickCheck", tabButtons, xRight - 6, yHead + 9, "RIGHTCLICK", "RIGHTCLICK_TIP", function() + HealBox.RightClick = RightClickCheck:GetChecked() and 1 or 0; + FBHealBox_ApplyRightClickLayout(); + FBHealBoxButtonsChanged(); + end); + RightClickCheck:SetChecked(nil); for i = 1, MaxButtonCount do local yPos = y0 - ((i - 1) * rowH); local btnIndex = i; - local btn = FBHealBox_CreatePickButton("FBHealBoxBtn"..i, tabButtons, xLeft, yPos, fieldW, 28, 20); + local btn = FBHealBox_CreatePickButton("FBHealBoxBtn"..i, tabButtons, xLeft, yPos, fieldW, fieldH, 20); btn.text:SetText(FBT("SELECT_SPELL")); btn.label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal"); btn.label:SetPoint("RIGHT", btn, "LEFT", -8, 0); @@ -2462,7 +3213,7 @@ function FBHealBoxCreateAddonOptionFrame() end); FBSpellBtns[i] = btn; - local btnR = FBHealBox_CreatePickButton("FBHealBoxBtnR"..i, tabButtons, xRight, yPos, fieldW, 28, 20); + local btnR = FBHealBox_CreatePickButton("FBHealBoxBtnR"..i, tabButtons, xRight, yPos, fieldW, fieldH, 20); btnR.text:SetText(FBT("SELECT_SPELL")); btnR:SetScript("OnClick", function() if (FBDragSpell and FBHealBox_DropSpell(btnIndex, "R")) then return; end @@ -2474,10 +3225,11 @@ function FBHealBoxCreateAddonOptionFrame() FBSpellBtnsR[i] = btnR; end - local yBelow = y0 - (MaxButtonCount * rowH) - 30; -- unter der letzten Zeile + local yBelow = y0 - (MaxButtonCount * rowH) - 26; -- unter der letzten Zeile + -- Buttonzahl: ueber die volle Breite beider Feldspalten, buendig mit den Feldern MaxButtonSlider = CreateFrame("Slider", "MaxButtonSlider", tabButtons, "OptionsSliderTemplate"); - MaxButtonSlider:SetWidth(fieldW); + MaxButtonSlider:SetWidth(xRight + fieldW - xLeft); MaxButtonSlider:SetHeight(16); MaxButtonSlider:SetPoint("TOPLEFT", xLeft, yBelow); MaxButtonSlider:SetMinMaxValues(0, MaxButtonCount); @@ -2490,14 +3242,6 @@ function FBHealBoxCreateAddonOptionFrame() getglobal(MaxButtonSlider:GetName() .. "High"):SetText(tostring(MaxButtonCount)); MaxButtonSlider:SetScript("OnValueChanged", MaxButtonSlider_Update); - -- Rechtsklick-Zweitzauber: bewusst nur hier einschaltbar, Standard aus - RightClickCheck = FBHealBox_CreateCheck("FBHealBoxRightClickCheck", tabButtons, xRight - 6, yBelow + 6, "RIGHTCLICK", "RIGHTCLICK_TIP", function() - HealBox.RightClick = RightClickCheck:GetChecked() and 1 or 0; - FBHealBox_ApplyRightClickLayout(); - FBHealBoxButtonsChanged(); - end); - RightClickCheck:SetChecked(nil); - -- ====================================================================== -- Reiter 2: Allgemeine Einstellungen -- ====================================================================== @@ -2622,8 +3366,32 @@ function FBHealBoxCreateAddonOptionFrame() end); BuffWatchPetsCheck:SetChecked(nil); + AggroMarkCheck = FBHealBox_CreateCheck("FBHealBoxAggroMarkCheck", tabGeneral, 40, cy - 150, "AGGRO", "AGGRO_TIP", function() + HealBox.AggroMark = AggroMarkCheck:GetChecked() and 1 or 0; + FBHealBox_CheckAggroAll(); + end); + AggroMarkCheck:SetChecked(1); + + SpellTimersCheck = FBHealBox_CreateCheck("FBHealBoxSpellTimersCheck", tabGeneral, 250, cy - 150, "TIMERS", "TIMERS_TIP", function() + HealBox.SpellTimers = SpellTimersCheck:GetChecked() and 1 or 0; + FBHealBox_UpdateSpellTimers(); + end); + SpellTimersCheck:SetChecked(1); + + CooldownsCheck = FBHealBox_CreateCheck("FBHealBoxCooldownsCheck", tabGeneral, 40, cy - 180, "COOLDOWNS", "COOLDOWNS_TIP", function() + HealBox.Cooldowns = CooldownsCheck:GetChecked() and 1 or 0; + FBHealBox_UpdateAllCooldowns(); + end); + CooldownsCheck:SetChecked(1); + + BuffIconsCheck = FBHealBox_CreateCheck("FBHealBoxBuffIconsCheck", tabGeneral, 250, cy - 180, "BUFFICONS", "BUFFICONS_TIP", function() + HealBox.BuffIcons = BuffIconsCheck:GetChecked() and 1 or 0; + FBHealBox_UpdateAllBuffIcons(); + end); + BuffIconsCheck:SetChecked(1); + -- [ Sprache und Buff-Wache ] ------------------------------------------- - local py = cy - 170; + local py = cy - 215; FBLangBtn = FBHealBox_CreatePickButton("FBHealBoxLangBtn", tabGeneral, 35, py, 180, 26, 18); FBLangBtn.icon:SetTexture("Interface\\Icons\\INV_Misc_Note_01"); FBLangBtn:SetScript("OnEnter", function() @@ -2722,6 +3490,11 @@ function FBHealBox_CreateCheck(name, parent, x, y, labelKey, tipKey, onClick) c:SetPoint("TOPLEFT", x, y); c.Text = c:CreateFontString(nil, "BACKGROUND", "GameFontNormal"); c.Text:SetPoint("LEFT", c, "RIGHT", 4, 1); + -- feste Breite und eine Zeile: lange Uebersetzungen enden mit "...", der + -- volle Text steht im Tooltip + c.Text:SetWidth(FBOPT_CHECK_TEXT_W); + c.Text:SetHeight(14); + c.Text:SetJustifyH("LEFT"); c.Text:SetText(FBT(labelKey)); c.labelKey = labelKey; c.tooltipText = FBT(tipKey); @@ -2795,6 +3568,7 @@ function FBHealBoxButtonsChanged() end end if (i <= (HealBox.MaxButtons or 0)) then b:Show(); end + FBHealBox_UpdateButtonCooldown(b); end end end @@ -2831,6 +3605,12 @@ function FBUpdateScaleSliderText() end end +function FBUpdateSmartMarginText() + if (SmartMarginSlider and SmartMarginSlider.Text) then + SmartMarginSlider.Text:SetText(format(FBT("SMART_MARGIN"), math.floor(SmartMarginSlider:GetValue() + 0.5))); + end +end + function FBUpdateSpacingSliderText() if (ButtonSpacingSlider and ButtonSpacingSlider.Text) then ButtonSpacingSlider.Text:SetText(format(FBT("BTN_SPACING"), math.floor(ButtonSpacingSlider:GetValue() + 0.5))); @@ -2930,6 +3710,9 @@ end function HealBoxButton_OnEvent(this, event, arg1) if (HealBox.Active == 0) then return 0; end + if (event == "SPELL_UPDATE_COOLDOWN" or event == "SPELL_UPDATE_USABLE") then + FBHealBox_UpdateButtonCooldown(this); + end if event=="SPELL_UPDATE_USABLE" then if (this.id) then local isUsable, noMana = IsUsableSpell(this.id, BOOKTYPE_SPELL); @@ -3060,6 +3843,8 @@ FBPredictTickInterval = { FBHoTs = {}; -- [Name] = { [Zauber] = {rank, perTick, interval, expires} } FBShields = {}; -- [Name] = { spell, rank, max, absorbed, expires } +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 } FBPredictWatch = {}; -- [Zauber] = { tex, bookID, rank, hasDirect, hasHoT, hasShield } @@ -3115,6 +3900,56 @@ end -- Wertet den Zauberbuch-Tooltip aus. Ein Zauber kann mehrere Anteile -- haben (Regrowth: Sofortheilung UND HoT), deshalb keine Entweder-Oder- -- Klassifizierung, sondern drei unabhaengige Felder. +-- Ist das Wort hinter einer Zahl eine Zeiteinheit? ("sec", "min", "hr", ...) +function FBPredict_IsTimeUnit(word) + if (not word) then return false; end + word = string.lower(word); + return (string.find(word, "^sec") or string.find(word, "^min") or string.find(word, "^hr") + or string.find(word, "^hour") or string.find(word, "^sek") or string.find(word, "^std") + or string.find(word, "^s$")) ~= nil; +end + +-- Laufzeit eines Buffs in Sekunden ("for 30 min", "Lasts 3 min", "1 hr", +-- "for 15 sec") oder nil. Die groesste gefundene Angabe gewinnt, damit +-- nicht eine Zauberzeit ("1.5 sec cast") erwischt wird. +function FBPredict_FindBuffDuration(txt) + if (not txt) then return nil; end + local best = nil; + for n, unit in string.gfind(txt, "(%d+)%s+(%a+)") do + local u = string.lower(unit); + local secs = nil; + if (string.find(u, "^min")) then secs = tonumber(n) * 60; + elseif (string.find(u, "^hr") or string.find(u, "^hour") or string.find(u, "^std")) then secs = tonumber(n) * 3600; + elseif (string.find(u, "^sec") or string.find(u, "^sek")) then secs = tonumber(n); end + if (secs and ((not best) or secs > best)) then best = secs; end + end + return best; +end + +-- "X to Y" ohne Zeiteinheit dahinter -> X, Y (Zahlen) oder nil +function FBPredict_FindAmountRange(txt) + if (not txt) then return nil; end + local pos = 1; + while true do + local s, e, lo, hi, tail = string.find(txt, "(%d+)%s+to%s+(%d+)%s*(%a*)", pos); + if (not s) then return nil; end + if (not FBPredict_IsTimeUnit(tail)) then return tonumber(lo), tonumber(hi); end + pos = e + 1; + end +end + +-- "for X" ohne Zeiteinheit dahinter -> X (Zahl) oder nil +function FBPredict_FindAmountSingle(txt) + if (not txt) then return nil; end + local pos = 1; + while true do + local s, e, n, tail = string.find(txt, "for%s+(%d+)%s*(%a*)", pos); + if (not s) then return nil; end + if (not FBPredict_IsTimeUnit(tail)) then return tonumber(n); end + pos = e + 1; + end +end + function FBPredict_GetSpellInfo(bookID, spellName) if (not bookID) then return nil; end if (FBPredictInfo[bookID] ~= nil) then @@ -3177,16 +4012,31 @@ function FBPredict_GetSpellInfo(bookID, spellName) end -- --- Sofortheilung --- - -- "for 887 to 1033" -> Mittelwert - local lo, hi = FBPredict_Find2(txt, { "(%d+)%s+to%s+(%d+)" }); + -- "for 887 to 1033" -> Mittelwert. Zeitangaben ("for 30 min", + -- "for 15 sec", "2 to 3 sec") sind keine Heilbetraege. + local lo, hi = FBPredict_FindAmountRange(txt); if (lo and hi) then - info.direct = (tonumber(lo) + tonumber(hi)) / 2; + info.direct = (lo + hi) / 2; found = true; else -- Einzelwert, aber nicht den HoT-Betrag doppelt zaehlen - local one = FBPredict_Find1(txt, { "for%s+(%d+)" }); - if (one) and ((not info.hot) or (tonumber(one) ~= info.hot.total)) then - info.direct = tonumber(one); + local one = FBPredict_FindAmountSingle(txt); + if (one) and ((not info.hot) or (one ~= info.hot.total)) then + info.direct = one; + found = true; + end + end + -- Heilt der Zauber ueberhaupt? (Buffs wie Seelenstaerke haben Zahlen, + -- aber kein "heal" im Text; Smart Healing verlangt das.) + info.isHeal = (string.find(txt, "[Hh]eal") ~= nil); + + -- --- Buff mit Laufzeit (Seelenstaerke "for 30 min", Furchtzauberschutz + -- "Lasts 3 min", Inneres Feuer "10 min"): nur, wenn es kein HoT, kein + -- Schild und keine Heilung ist. Liefert die Restlaufzeit auf dem Button. + if (not info.hot) and (not info.shield) and (not info.isHeal) then + local secs = FBPredict_FindBuffDuration(txt); + if (secs and secs >= 30) then + info.buff = { duration = secs }; found = true; end end @@ -3215,11 +4065,39 @@ function FBPredict_BuildWatch() hasDirect = (info.direct ~= nil), hasHoT = (info.hot ~= nil), hasShield = (info.shield ~= nil), + hasBuff = (info.buff ~= nil), + buffSecs = info.buff and info.buff.duration, + icon = top.icon, }; end end end +-- Buff-Laufzeit auf einer Einheit vermerken +function FBPredict_StartBuff(unitName, spellName, secs) + if (not unitName) or (not spellName) or (not secs) then return; end + if (not FBBuffTimers[unitName]) then FBBuffTimers[unitName] = {}; end + FBBuffTimers[unitName][spellName] = { expires = GetTime() + secs }; +end + +-- Restlaufzeit eines eigenen Buffs (Textur) ueber die Spielerbuff-API +function FBPredict_PlayerBuffTimeLeft(tex) + if (not GetPlayerBuff) or (not GetPlayerBuffTexture) or (not GetPlayerBuffTimeLeft) then return nil; end + local i = 0; + while true do + local id = GetPlayerBuff(i, "HELPFUL"); + if (not id) or (id < 0) then break; end + local t = GetPlayerBuffTexture(id); + if (t and strupper(t) == tex) then + local left = GetPlayerBuffTimeLeft(id); + if (left and left > 0) then return left; end + return nil; + end + i = i + 1; + end + return nil; +end + -- [ Lernspeicher (wandert in die SavedVariables) ] -------------------------- FBPREDICT_ABSORB_MAX_FACTOR = 1.5; @@ -3321,6 +4199,7 @@ function FBPredict_StartShield(unitName, spellName, rank, bookID, rankKnown) rank = rank, rankKnown = rankKnown, tooltip = info.shield.amount, + duration = info.shield.duration, max = amount, absorbed = 0, expires = GetTime() + info.shield.duration, @@ -3497,10 +4376,33 @@ function FBPredict_ScanUnit(unit) dirty = FBPredict_StartShield(name, FBPredictPending.spell, FBPredictPending.rank, FBPredictPending.bookID, true) or dirty; end + if (w.hasBuff) then + FBPredict_StartBuff(name, FBPredictPending.spell, w.buffSecs); + end FBPredictPending = nil; end end + -- Buff-Laufzeiten: beim Spieler exakt aus der Spielerbuff-API, bei + -- anderen nur eigene Casts (oben); weg ist weg. Praesenz merken, damit + -- die Plakette auch fremde Buffs (ohne Uhr) zeigen kann. + if (not FBBuffPresent[name]) then FBBuffPresent[name] = {}; end + for spellName, w in pairs(FBPredictWatch) do + if (w.hasBuff) then + local present = textures[w.tex]; + FBBuffPresent[name][spellName] = present; + local tracked = FBBuffTimers[name] and FBBuffTimers[name][spellName]; + if (present and unit == "player") then + -- beim Spieler bei jedem Scan neu aus der Buff-API lesen: so + -- stellt auch ein Neucast per Aktionsleiste die Uhr auf voll + local left = FBPredict_PlayerBuffTimeLeft(w.tex); + if (left) then FBPredict_StartBuff(name, spellName, left); end + elseif (tracked and not present) then + FBBuffTimers[name][spellName] = nil; + end + end + end + -- 2) Generischer Abgleich: Buff da / Buff weg for spellName, w in pairs(FBPredictWatch) do local present = textures[w.tex]; @@ -3710,6 +4612,7 @@ end FBPredictEvents = { "UNIT_AURA", + "PLAYER_AURAS_CHANGED", -- eigene Buffs: 1.12 feuert hierfuer kein UNIT_AURA("player") "CHAT_MSG_ADDON", "UI_ERROR_MESSAGE", -- Sichtlinien-Fehler -- eigener Cast @@ -3749,6 +4652,9 @@ FBPredictFrame:SetScript("OnEvent", function() if (event == "UNIT_AURA") then FBPredict_ScanUnit(arg1); + elseif (event == "PLAYER_AURAS_CHANGED") then + FBPredict_ScanUnit("player"); + elseif (event == "CHAT_MSG_ADDON") then FBComm_OnMessage(arg1, arg2, arg3, arg4); @@ -3795,6 +4701,11 @@ function FBPredict_OnUpdate(elapsed) if (FBPredictAccum < FBPREDICT_THROTTLE) then return; end FBPredictAccum = 0; + -- Angegriffenen markieren, Button-Timer und Buff-Icons nachfuehren (0.2-s-Takt) + FBHealBox_CheckAggroAll(); + FBHealBox_UpdateSpellTimers(); + FBHealBox_UpdateAllBuffIcons(); + local now = GetTime(); local dirty = false; @@ -4075,6 +4986,40 @@ SlashCmdList["FBHEALPREDICT"] = function(msg) return; end + if (msg == "buffs") then + -- Diagnose: ueberwachte Buffs und ihr Zustand auf dem Spieler + local me = UnitName("player"); + local any = false; + for spellName, w in pairs(FBPredictWatch) do + if (w.hasBuff) then + any = true; + local present = FBBuffPresent[me] and FBBuffPresent[me][spellName]; + local bt = FBBuffTimers[me] and FBBuffTimers[me][spellName]; + local left = "-"; + if (bt) then left = math.floor(bt.expires - GetTime()).."s"; end + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r buff "..spellName.." ("..(w.buffSecs or 0).."s) tex="..tostring(w.tex) + .." present="..tostring(present ~= nil and present or false).." left="..left); + end + end + if (not any) then DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r no buff spells on the buttons (assign e.g. Power Word: Fortitude)"); end + if (FBHealBox1 and FBHealBox1.buffIcons) then + for k, ic in ipairs(FBHealBox1.buffIcons) do + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r icon "..k..": shown="..tostring(ic:IsShown() and true or false) + .." name="..tostring(ic.buffName).." left="..tostring(ic.buffLeft and math.floor(ic.buffLeft)) + .." grey="..tostring(ic.greyCount).." q1shown="..tostring(ic.q[1]:IsShown() and true or false) + .." desat="..tostring(ic.q[1].fbDesat)); + end + end + local i = 1; + while (i <= 32) do + local tex = UnitBuff("player", i); + if (not tex) then break; end + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r player buff "..i..": "..tex); + i = i + 1; + end + return; + end + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..FBT("FBP_WATCHED")); for spellName, w in pairs(FBPredictWatch) do local info = FBPredict_GetSpellInfo(w.bookID, spellName); @@ -4136,6 +5081,9 @@ SlashCmdList["FBHEALPREDICT"] = function(msg) end end end + local smart = FBT("FBP_STATE_OFF"); + if (HealBox.SmartRank == 1) then smart = FBT("FBP_STATE_ON"); end + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..format(FBT("FBP_SMART"), smart, HealBox.SmartMargin or 20)); FBHealBox_RunHook("Status"); DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..FBT("FBP_COMMANDS")); end @@ -4161,6 +5109,11 @@ end function FBHealBox_DispelType(unit) local g = FBTest_Ghost(unit); if (g) then + -- Geist mit festem Debuff-Typ (nur, wenn die eigene Klasse ihn entfernt) + if (g.debuffType) then + if (FBDispelColors[g.debuffType]) then return g.debuffType, g.debuffTex, g.debuffCount; end + return nil; + end -- Geist mit Debuff: irgendein Typ, den die eigene Klasse entfernen kann if (g.debuff) then for _, dtype in ipairs({ "Magic", "Poison", "Disease", "Curse" }) do @@ -4181,7 +5134,8 @@ end function FBHealBox_UpdateDebuffIcon(frame, texture, count) if (not frame.DebuffIcon) then return; end if (texture and HealBox.DebuffIcon == 1 and not frame.plateHidden) then - frame.NameText:SetWidth(frame.nameWidthIcon or FBNAME_WIDTH_ICON); + frame.debuffShown = true; + FBHealBox_ApplyNameWidth(frame); frame.DebuffIcon:SetTexture(texture); frame.DebuffIcon:Show(); if (count and tonumber(count) and tonumber(count) > 1) then @@ -4191,12 +5145,224 @@ function FBHealBox_UpdateDebuffIcon(frame, texture, count) frame.DebuffCount:Hide(); end else - frame.NameText:SetWidth(frame.nameWidthFull or FBNAME_WIDTH_FULL); + frame.debuffShown = false; + FBHealBox_ApplyNameWidth(frame); frame.DebuffIcon:Hide(); frame.DebuffCount:Hide(); end end +-- Namensbreite: Grundbreite (mit/ohne Debuff-Icon) minus Platz der Buff-Icons +function FBHealBox_ApplyNameWidth(frame) + if (not frame) or (not frame.NameText) then return; end + local w = frame.nameWidthFull or FBNAME_WIDTH_FULL; + if (frame.debuffShown) then w = frame.nameWidthIcon or FBNAME_WIDTH_ICON; end + if (w < 16) then w = 16; end + frame.NameText:SetWidth(w); +end + +-- ========================================================================== +-- [ Buff-Icons mit Uhr im Lebensbalken ] +-- +-- Buffs mit Laufzeit von den Buttons (Seelenstaerke, Willen, ...), die auf +-- der Einheit liegen, als kleine Icons links vom Prozenttext. Restzeit +-- bekannt (eigener Cast, eigene Buffs): Cooldown-Uhr laeuft; unbekannt +-- (fremder Cast): Icon ohne Uhr. Der Name macht Platz. +-- ========================================================================== + +-- Quadranten der Uhr im Uhrzeigersinn ab 12 Uhr: Ecke, Texturausschnitt +FBBUFFICON_QUADS = { + { corner = "TOPRIGHT", u1 = 0.50, u2 = 0.93, v1 = 0.07, v2 = 0.50 }, + { corner = "BOTTOMRIGHT", u1 = 0.50, u2 = 0.93, v1 = 0.50, v2 = 0.93 }, + { corner = "BOTTOMLEFT", u1 = 0.07, u2 = 0.50, v1 = 0.50, v2 = 0.93 }, + { corner = "TOPLEFT", u1 = 0.07, u2 = 0.50, v1 = 0.07, v2 = 0.50 }, +}; + +-- Icon Nr. k anlegen, aussen links neben der Plakette, in Zweierstapeln: +-- Icon 1 oben an der Kante, Icon 2 darunter, Icon 3 oben in der naechsten +-- Spalte links usw. Farbiges Icon plus vier schwarz-weisse Quadranten. +function FBHealBox_GetBuffIcon(frame, k, size, rows) + if (not frame.buffIcons) then frame.buffIcons = {}; end + if (frame.buffIcons[k]) then return frame.buffIcons[k]; end + size = size or FBBUFFICON_SIZE; + rows = rows or FBBUFFICON_ROWS; + local ic = CreateFrame("Frame", nil, frame); + ic:SetWidth(size); + ic:SetHeight(size); + local col = math.floor((k - 1) / rows); + local row = math.mod(k - 1, rows); + local stackH = rows * size + (rows - 1) * FBBUFFICON_GAP; + local x = FBBUFFICON_XOFF - col * (size + FBBUFFICON_GAP); + local y = stackH / 2 - row * (size + FBBUFFICON_GAP); -- Oberkante der Zeile relativ zur Mitte + ic:SetPoint("TOPRIGHT", frame, "LEFT", x, y); + if (ic.SetFrameLevel and frame.GetFrameLevel) then ic:SetFrameLevel((frame:GetFrameLevel() or 0) + 5); end + ic.tex = ic:CreateTexture(nil, "ARTWORK"); + ic.tex:SetAllPoints(ic); + ic.tex:SetTexCoord(0.07, 0.93, 0.07, 0.93); + -- Die grauen Quadranten liegen auf einem eigenen Kind-Frame eine Ebene + -- ueber dem Icon: Frame-Ebenen zeichnet der 1.12-Client verlaesslich in + -- Reihenfolge, Textur-Layer innerhalb eines Frames nicht in jedem Fall. + ic.grey = CreateFrame("Frame", nil, ic); + ic.grey:SetAllPoints(ic); + if (ic.grey.SetFrameLevel and ic.GetFrameLevel) then ic.grey:SetFrameLevel((ic:GetFrameLevel() or 0) + 1); end + ic.q = {}; + ic.w = {}; + local half = size / 2; + for i, qd in ipairs(FBBUFFICON_QUADS) do + -- schwarz-weisser Ausschnitt des Icons ... + local t = ic.grey:CreateTexture(nil, "ARTWORK"); + t:SetWidth(half); + t:SetHeight(half); + t:SetPoint(qd.corner, ic, qd.corner, 0, 0); + t:SetTexCoord(qd.u1, qd.u2, qd.v1, qd.v2); + t:Hide(); + ic.q[i] = t; + -- ... plus dunkle Waesche darueber, damit der Unterschied auf jedem Client sichtbar ist + local wsh = ic.grey:CreateTexture(nil, "OVERLAY"); + wsh:SetWidth(half); + wsh:SetHeight(half); + wsh:SetPoint(qd.corner, ic, qd.corner, 0, 0); + wsh:SetTexture(FBBUFFICON_WASH[1], FBBUFFICON_WASH[2], FBBUFFICON_WASH[3], FBBUFFICON_WASH[4]); + wsh:Hide(); + ic.w[i] = wsh; + end + -- Tooltip: Buffname und Restzeit (auch zur Diagnose) + ic:EnableMouse(true); + ic:SetScript("OnEnter", function() + if (not this.buffName) then return; end + GameTooltip:SetOwner(this, "ANCHOR_RIGHT"); + GameTooltip:SetText(this.buffName); + local left = this.buffLeft; + if (left) then + GameTooltip:AddLine(FBHealBox_FormatTimer(left), 1, 1, 1); + else + GameTooltip:AddLine(FBT("TT_BUFF_UNKNOWN"), 0.7, 0.7, 0.7); + end + GameTooltip:Show(); + end); + ic:SetScript("OnLeave", function() GameTooltip:Hide(); end); + ic:Hide(); + frame.buffIcons[k] = ic; + return ic; +end + +-- Quadrant schwarz-weiss machen: Entsaettigung (wenn der Client sie kann), +-- sonst Grauton. Muss nach jedem SetTexture erneut gesetzt werden. +function FBHealBox_GreyTexture(t) + local desat = nil; + if (t.SetDesaturated) then desat = t:SetDesaturated(1); end + t.fbDesat = desat; + if (desat) then + t:SetVertexColor(0.55, 0.55, 0.55, 1); + else + t:SetVertexColor(FBBUFFICON_GREY, FBBUFFICON_GREY, FBBUFFICON_GREY, 1); + end +end + +-- Uhrstand setzen: remaining = Restanteil 0..1, nil = unbekannt (alles farbig) +function FBHealBox_SetBuffIconProgress(ic, remaining) + local grey = 0; + if (remaining) then + if (remaining < 0) then remaining = 0; end + if (remaining > 1) then remaining = 1; end + grey = math.floor((1 - remaining) * 4 + 0.5); -- 0..4 Quadranten grau, ab 12 Uhr im Uhrzeigersinn + if (grey > 4) then grey = 4; end + end + if (ic.greyCount == grey) then return; end + ic.greyCount = grey; + for i = 1, 4 do + if (i <= grey) then ic.q[i]:Show(); ic.w[i]:Show(); else ic.q[i]:Hide(); ic.w[i]:Hide(); end + end +end + +-- Liste der zu zeigenden Buffs einer Einheit: { tex, expires, duration } +function FBHealBox_BuffIconList(unitName, g) + local list = {}; + if (g) then + for _, b in ipairs(g.buffs or {}) do + table.insert(list, { tex = b.tex, expires = b.left and (GetTime() + b.left), duration = b.dur, name = "Test" }); + end + return list; + end + if (not unitName) then return list; end + local present = FBBuffPresent[unitName]; + if (not present) then return list; end + local names = {}; + for spellName, w in pairs(FBPredictWatch) do + if (w.hasBuff and present[spellName]) then table.insert(names, spellName); end + end + table.sort(names); + for _, spellName in ipairs(names) do + local w = FBPredictWatch[spellName]; + local bt = FBBuffTimers[unitName] and FBBuffTimers[unitName][spellName]; + table.insert(list, { tex = w.icon, expires = bt and bt.expires, duration = w.buffSecs, name = spellName }); + end + return list; +end + +-- Icons einer Plakette (oder Raid-Zelle) nachfuehren. Liefert die Anzahl. +-- size, rows, maxIcons: Icongroesse, Stapelhoehe und Hoechstzahl (Plaketten: +-- 8 px, 2 hoch, 6; Raid-Zellen: 6 px, 3 hoch, 12). +function FBHealBox_UpdateBuffIcons(frame, unitName, g, size, rows, maxIcons) + if (not frame) or (not frame.HPText) then return 0; end + maxIcons = maxIcons or FBBUFFICON_MAX; + local list = {}; + if (HealBox.BuffIcons == 1 and not frame.plateHidden) then + list = FBHealBox_BuffIconList(unitName, g); + end + local now = GetTime(); + local n = 0; + for k = 1, maxIcons do + local b = list[k]; + local ic = (frame.buffIcons and frame.buffIcons[k]) or (b and FBHealBox_GetBuffIcon(frame, k, size, rows)); + if (ic) then + if (b and b.tex) then + n = n + 1; + if (ic.lastTex ~= b.tex) then + ic.lastTex = b.tex; + ic.tex:SetTexture(b.tex); + for i = 1, 4 do + ic.q[i]:SetTexture(b.tex); + FBHealBox_GreyTexture(ic.q[i]); + end + ic.greyCount = nil; + end + -- Uhr: Restanteil bekannt -> Quadranten; unbekannt -> alles farbig + local remaining = nil; + ic.buffName = b.name; + ic.buffLeft = nil; + if (b.expires and b.duration and b.duration > 0) then + remaining = (b.expires - now) / b.duration; + ic.buffLeft = b.expires - now; + end + FBHealBox_SetBuffIconProgress(ic, remaining); + ic:Show(); + else + ic:Hide(); + ic.lastTex = nil; + ic.greyCount = nil; + end + end + end + frame.buffIconCount = n; + return n; +end + +function FBHealBox_UpdateAllBuffIcons() + for p = 1, FBSlotCount do + local f = FBPartyFrame[p]; + local unit = FBPartyUnit[p]; + if (f) then + if (f:IsShown() and FBUnitExists(unit)) then + FBHealBox_UpdateBuffIcons(f, FBUnitName(unit), FBTest_Ghost(unit), FBBUFFICON_SIZE); + else + FBHealBox_UpdateBuffIcons(f, nil, nil, FBBUFFICON_SIZE); + end + end + end + FBHealBox_RunHook("BuffIcons"); +end + -- Manabalken: nur wenn eingeschaltet, die Plakette sichtbar ist und die -- Einheit tatsaechlich Mana nutzt. Sonst weg, dann ist der Lebensbalken -- wieder auf voller Hoehe zu sehen. diff --git a/FBHealBox.toc b/FBHealBox.toc index 1820aaa..d7302ae 100644 --- a/FBHealBox.toc +++ b/FBHealBox.toc @@ -1,12 +1,14 @@ ## Interface: 11200 -## Title: Heal Box Vanilla |cFFFFFF00v1.4.2|r -## Notes: Party, pet and self heal display with quick-cast buttons for healers. Ported to client 1.12.1 (optional SuperWoW). Up to 10 buttons per row, mana bar inside the health bar, class colours, buff watch, range fading, adjustable spacing, test mode with ghost players, optional raid mode for 20 and 40 players. Heal prediction for direct heals, HoT ticks and absorb shields, self-correcting from the combat log. HealComm sync with Puppeteer, pfUI and others. German/English. Original by Dourd, ported to Vanilla and extended 09/2026 by Mquadrat. -## Notes-deDE: Party-, Begleiter- und Selbst-Heilanzeige mit Schnellzugriff-Buttons fuer Heiler. Portiert fuer Client 1.12.1 (optional SuperWoW). Bis zu 10 Buttons pro Reihe, Manabalken im Lebensbalken, Klassenfarben, Buff-Wache, Reichweiten-Fading, einstellbare Abstaende, Testmodus mit Geisterspielern, optionaler Raidmodus fuer 20 und 40 Spieler. Heal-Prediction fuer Direktheilung, HoT-Ticks und Absorb-Schilde, selbstkorrigierend ueber den Combatlog. HealComm-Sync mit Puppeteer, pfUI und Co. Deutsch/Englisch. Original von Dourd, Vanilla-Portierung und Erweiterung 09/2026 von Mquadrat. +## Title: Heal Box Vanilla |cFFFFFF00v1.4.3|r +## Notes: Party, pet and self heal display with quick-cast buttons for healers. Ported to client 1.12.1 (optional SuperWoW). Up to 10 buttons per row, mana bar inside the health bar, class colours, buff watch, range fading, adjustable spacing, test mode with ghost players, optional raid mode for 20 and 40 players, mana ticker with five-second rule, smart damage ranking for attack spells. Heal prediction for direct heals, HoT ticks and absorb shields, self-correcting from the combat log. HealComm sync with Puppeteer, pfUI and others. German/English. Original by Dourd, ported to Vanilla and extended 09/2026 by Mquadrat. +## Notes-deDE: Party-, Begleiter- und Selbst-Heilanzeige mit Schnellzugriff-Buttons fuer Heiler. Portiert fuer Client 1.12.1 (optional SuperWoW). Bis zu 10 Buttons pro Reihe, Manabalken im Lebensbalken, Klassenfarben, Buff-Wache, Reichweiten-Fading, einstellbare Abstaende, Testmodus mit Geisterspielern, optionaler Raidmodus fuer 20 und 40 Spieler, Mana-Ticker mit Fuenf-Sekunden-Regel, Smart Damage fuer Angriffszauber. Heal-Prediction fuer Direktheilung, HoT-Ticks und Absorb-Schilde, selbstkorrigierend ueber den Combatlog. HealComm-Sync mit Puppeteer, pfUI und Co. Deutsch/Englisch. Original von Dourd, Vanilla-Portierung und Erweiterung 09/2026 von Mquadrat. ## Author: Dourd (Argent Dawn EU) | Vanilla-Port & Erweiterung 09/2026: Mquadrat -## Version: 1.4.2 +## Version: 1.4.3 ## SavedVariablesPerCharacter: HealBox ## OptionalDeps: SuperWoW FBHealBox.lua FBHealBox_Raid.lua +FBHealBox_Ticker.lua +FBHealBox_Damage.lua FBHealBox.xml diff --git a/FBHealBox_Damage.lua b/FBHealBox_Damage.lua new file mode 100644 index 0000000..d3c2581 --- /dev/null +++ b/FBHealBox_Damage.lua @@ -0,0 +1,617 @@ +-- ========================================================================== +-- Heal Box Vanilla: Smart Damage (Modul, ab v1.4.3) +-- +-- Rangwahl fuer Angriffszauber auf jeder Aktionsleiste. Bongos, Blizzard- +-- Leisten und Tastenkuerzel rufen UseAction(slot) auf; ein Vor-Hook erkennt +-- den Zauber im Slot per Tooltip-Scan und wirkt, wenn ein kleinerer Rang +-- den Gegner sicher toetet, diesen Rang per CastSpellByName. Sonst laeuft +-- der Aufruf unveraendert weiter. Nie ueber dem Rang auf der Leiste. +-- +-- Leben des Gegners, vier Quellen mit Vorrang: +-- 1) Server sendet echte Werte (UnitHealthMax ~= 100) +-- 2) MobHealth3 MobHealth3:GetUnitHealth(unit) +-- 3) MobInfo-2 MobHealth_GetTargetCurHP()/MaxHP() +-- 4) eigene Schaetzung Lebenspunkte je Prozent aus beobachtetem Schaden +-- (eigener, Gruppe, Begleiter) und dem Prozentabfall des Ziels, erst ab +-- FBDMG_MIN_DROP Prozent Abfall, Maximum ueber alle Messungen (Fehler +-- nur nach oben, also in die sichere Richtung), gemerkt je Name:Stufe. +-- +-- Schaden je Rang: Mindestschaden aus dem Tooltip ("86 to 98 Holy damage"), +-- nachgelernt aus eigenen Treffern ohne Teilwiderstand. Entscheidung: +-- Mindestschaden >= Ziel-Leben (bei Prozent: Obergrenze) x (1 + Aufschlag). +-- ========================================================================== + +if (FBHealBox_DamageLoaded) then return; end +FBHealBox_DamageLoaded = true; + +FBDMG_MIN_DROP = 3; -- Prozent Abfall, ab dem eine Messung zaehlt +FBDMG_LASTCAST_SEC = 4; -- Sek., wie lange ein eigener Cast fuers Lernen gilt + +FBDamageDefaults = { + Enabled = 0, -- Smart Damage an/aus (bewusst aus) + Margin = 20, -- Sicherheitsaufschlag in Prozent +}; + +-- Angriffszauber je Klasse (englische Zaubernamen wie im ganzen Addon). +-- Kanalisierte, reine DoTs und Flaechenzauber bleiben draussen. +FBDamageSpells = { + Priest = { "Smite", "Holy Fire", "Mind Blast" }, + Druid = { "Wrath", "Starfire", "Moonfire" }, + Shaman = { "Lightning Bolt", "Chain Lightning", "Earth Shock", "Flame Shock", "Frost Shock" }, + Paladin = { "Holy Shock", "Hammer of Wrath", "Exorcism", "Holy Wrath" }, + Mage = { "Fireball", "Frostbolt", "Fire Blast", "Scorch", "Pyroblast" }, + Warlock = { "Shadow Bolt", "Searing Pain", "Immolate", "Soul Fire", "Conflagrate" }, + Hunter = { "Arcane Shot", "Aimed Shot" }, +}; + +-- ========================================================================== +-- [ Texte ] +-- ========================================================================== + +FBLocale["enUS"].DMG_HEADER = "Smart Damage"; +FBLocale["enUS"].DMG_ENABLED = "Smart Damage"; +FBLocale["enUS"].DMG_ENABLED_TIP = "What it does: when you press an attack spell on any action bar (Bongos, Blizzard bars, key bindings) and a lower rank of that spell would still kill the target, that rank is cast instead. Saves mana on nearly dead targets. Rules: never above the rank on the bar, minimum damage of the rank must cover the target's remaining health plus the safety margin. Needs the target's real health: from the server, from MobHealth3 or MobInfo-2, or from the addon's own estimate learned while fighting a mob type. Downsides: estimates can be wrong, partial resists lower the damage, and on unknown mobs nothing happens until enough has been learned. Off by default."; +FBLocale["enUS"].DMG_MARGIN = "Safety margin: |cFFFFFFFF%s %%"; +FBLocale["enUS"].DMG_SRC = "Target health source: |cFFFFFFFF%s"; +FBLocale["enUS"].DMG_SRC_REAL = "server sends real values"; +FBLocale["enUS"].DMG_SRC_MH3 = "MobHealth3"; +FBLocale["enUS"].DMG_SRC_MI2 = "MobInfo-2"; +FBLocale["enUS"].DMG_SRC_EST = "own estimate (%d mob types learned)"; +FBLocale["enUS"].DMG_SRC_NONE = "no target"; +FBLocale["enUS"].DMG_SRC_UNKNOWN = "percent only, nothing learned yet"; +FBLocale["enUS"].DMG_SPELLS = "Spells: |cFFFFFFFF%s"; +FBLocale["enUS"].DMG_INFO = "The target line above updates with your current target. Learning happens automatically: every fight against a mob type improves the estimate for that type."; +FBLocale["enUS"].DMG_STATUS = "Smart Damage: %s, margin %d %%, %s"; +FBLocale["enUS"].DBG_DMG = "Smart Damage: %s -> %s (target %d HP via %s, min damage %d)"; +FBLocale["enUS"].DBG_DMG_KEEP = "Smart Damage: %s kept (target %d HP via %s)"; +FBLocale["enUS"].DMG_LOADED = "Smart Damage module loaded: section in tab |cFFFFFFFFExtras|r, /fbp damage"; +FBLocale["enUS"].DMG_ON = "Smart Damage |cFF00FF00on|r."; +FBLocale["enUS"].DMG_OFF = "Smart Damage |cFFFF0000off|r."; + +FBLocale["deDE"].DMG_HEADER = "Smart Damage"; +FBLocale["deDE"].DMG_ENABLED = "Smart Damage"; +FBLocale["deDE"].DMG_ENABLED_TIP = "Was es tut: Drueckst du einen Angriffszauber auf einer beliebigen Aktionsleiste (Bongos, Blizzard-Leisten, Tastenkuerzel) und ein niedrigerer Rang wuerde das Ziel noch toeten, wird stattdessen dieser Rang gewirkt. Spart Mana bei fast toten Zielen. Regeln: nie ueber dem Rang auf der Leiste, der Mindestschaden des Rangs muss das Restleben des Ziels plus Sicherheitsaufschlag decken. Braucht das echte Leben des Ziels: vom Server, von MobHealth3 oder MobInfo-2, oder aus der eigenen Schaetzung, die das Addon im Kampf gegen einen Mobtyp lernt. Nachteile: Schaetzungen koennen danebenliegen, Teilwiderstaende senken den Schaden, und bei unbekannten Mobs passiert nichts, bis genug gelernt ist. Standardmaessig aus."; +FBLocale["deDE"].DMG_MARGIN = "Sicherheitsaufschlag: |cFFFFFFFF%s %%"; +FBLocale["deDE"].DMG_SRC = "Lebensquelle des Ziels: |cFFFFFFFF%s"; +FBLocale["deDE"].DMG_SRC_REAL = "Server sendet echte Werte"; +FBLocale["deDE"].DMG_SRC_MH3 = "MobHealth3"; +FBLocale["deDE"].DMG_SRC_MI2 = "MobInfo-2"; +FBLocale["deDE"].DMG_SRC_EST = "eigene Schaetzung (%d Mobtypen gelernt)"; +FBLocale["deDE"].DMG_SRC_NONE = "kein Ziel"; +FBLocale["deDE"].DMG_SRC_UNKNOWN = "nur Prozent, noch nichts gelernt"; +FBLocale["deDE"].DMG_SPELLS = "Zauber: |cFFFFFFFF%s"; +FBLocale["deDE"].DMG_INFO = "Die Zielzeile oben folgt deinem aktuellen Ziel. Gelernt wird von selbst: Jeder Kampf gegen einen Mobtyp verbessert die Schaetzung fuer diesen Typ."; +FBLocale["deDE"].DMG_STATUS = "Smart Damage: %s, Aufschlag %d %%, %s"; +FBLocale["deDE"].DBG_DMG = "Smart Damage: %s -> %s (Ziel %d LP ueber %s, Mindestschaden %d)"; +FBLocale["deDE"].DBG_DMG_KEEP = "Smart Damage: %s bleibt (Ziel %d LP ueber %s)"; +FBLocale["deDE"].DMG_LOADED = "Smart-Damage-Modul geladen: Abschnitt im Reiter |cFFFFFFFFExtras|r, /fbp damage"; +FBLocale["deDE"].DMG_ON = "Smart Damage |cFF00FF00an|r."; +FBLocale["deDE"].DMG_OFF = "Smart Damage |cFFFF0000aus|r."; + +-- Weitere Sprachen (Spanisch, Franzoesisch, Italienisch) + +FBLocale["esES"].DMG_HEADER = "Smart Damage"; +FBLocale["esES"].DMG_ENABLED = "Smart Damage"; +FBLocale["esES"].DMG_ENABLED_TIP = "Qué hace: cuando pulsas un hechizo de ataque en cualquier barra de acción (Bongos, barras de Blizzard, atajos de teclado) y un rango inferior de ese hechizo aún mataría al objetivo, se lanza ese rango en su lugar. Ahorra maná con objetivos casi muertos. Reglas: nunca por encima del rango de la barra, el daño mínimo del rango debe cubrir la vida restante del objetivo más el margen de seguridad. Necesita la vida real del objetivo: del servidor, de MobHealth3 o MobInfo-2, o de la estimación propia del addon aprendida al combatir un tipo de enemigo. Desventajas: las estimaciones pueden fallar, las resistencias parciales reducen el daño y con enemigos desconocidos no ocurre nada hasta haber aprendido lo suficiente. Desactivado por defecto."; +FBLocale["esES"].DMG_MARGIN = "Margen de seguridad: |cFFFFFFFF%s %%"; +FBLocale["esES"].DMG_SRC = "Fuente de vida del objetivo: |cFFFFFFFF%s"; +FBLocale["esES"].DMG_SRC_REAL = "el servidor envía valores reales"; +FBLocale["esES"].DMG_SRC_MH3 = "MobHealth3"; +FBLocale["esES"].DMG_SRC_MI2 = "MobInfo-2"; +FBLocale["esES"].DMG_SRC_EST = "estimación propia (%d tipos de enemigo aprendidos)"; +FBLocale["esES"].DMG_SRC_NONE = "sin objetivo"; +FBLocale["esES"].DMG_SRC_UNKNOWN = "solo porcentaje, aún nada aprendido"; +FBLocale["esES"].DMG_SPELLS = "Hechizos: |cFFFFFFFF%s"; +FBLocale["esES"].DMG_INFO = "La línea del objetivo se actualiza con tu objetivo actual. El aprendizaje es automático: cada combate contra un tipo de enemigo mejora la estimación de ese tipo."; +FBLocale["esES"].DMG_STATUS = "Smart Damage: %s, margen %d %%, %s"; +FBLocale["esES"].DBG_DMG = "Smart Damage: %s -> %s (objetivo %d PV vía %s, daño mínimo %d)"; +FBLocale["esES"].DBG_DMG_KEEP = "Smart Damage: %s mantenido (objetivo %d PV vía %s)"; +FBLocale["esES"].DMG_LOADED = "Módulo Smart Damage cargado: sección en la pestaña |cFFFFFFFFExtras|r, /fbp damage"; +FBLocale["esES"].DMG_ON = "Smart Damage |cFF00FF00activado|r."; +FBLocale["esES"].DMG_OFF = "Smart Damage |cFFFF0000desactivado|r."; + +FBLocale["frFR"].DMG_HEADER = "Smart Damage"; +FBLocale["frFR"].DMG_ENABLED = "Smart Damage"; +FBLocale["frFR"].DMG_ENABLED_TIP = "Ce que ça fait : quand vous appuyez sur un sort d'attaque sur n'importe quelle barre d'action (Bongos, barres Blizzard, raccourcis) et qu'un rang inférieur de ce sort tuerait encore la cible, c'est ce rang qui est lancé. Économise du mana sur les cibles presque mortes. Règles : jamais au-dessus du rang de la barre, les dégâts minimaux du rang doivent couvrir la vie restante de la cible plus la marge de sécurité. Nécessite la vie réelle de la cible : du serveur, de MobHealth3 ou MobInfo-2, ou de l'estimation propre de l'addon apprise en combattant un type de monstre. Inconvénients : les estimations peuvent se tromper, les résistances partielles réduisent les dégâts, et sur les monstres inconnus rien ne se passe tant que l'apprentissage est insuffisant. Désactivé par défaut."; +FBLocale["frFR"].DMG_MARGIN = "Marge de sécurité : |cFFFFFFFF%s %%"; +FBLocale["frFR"].DMG_SRC = "Source de vie de la cible : |cFFFFFFFF%s"; +FBLocale["frFR"].DMG_SRC_REAL = "le serveur envoie des valeurs réelles"; +FBLocale["frFR"].DMG_SRC_MH3 = "MobHealth3"; +FBLocale["frFR"].DMG_SRC_MI2 = "MobInfo-2"; +FBLocale["frFR"].DMG_SRC_EST = "estimation propre (%d types de monstres appris)"; +FBLocale["frFR"].DMG_SRC_NONE = "aucune cible"; +FBLocale["frFR"].DMG_SRC_UNKNOWN = "pourcentage seulement, rien appris pour le moment"; +FBLocale["frFR"].DMG_SPELLS = "Sorts : |cFFFFFFFF%s"; +FBLocale["frFR"].DMG_INFO = "La ligne de cible ci-dessus se met à jour avec votre cible actuelle. L'apprentissage est automatique : chaque combat contre un type de monstre améliore l'estimation pour ce type."; +FBLocale["frFR"].DMG_STATUS = "Smart Damage : %s, marge %d %%, %s"; +FBLocale["frFR"].DBG_DMG = "Smart Damage : %s -> %s (cible %d PV via %s, dégâts min %d)"; +FBLocale["frFR"].DBG_DMG_KEEP = "Smart Damage : %s conservé (cible %d PV via %s)"; +FBLocale["frFR"].DMG_LOADED = "Module Smart Damage chargé : section dans l'onglet |cFFFFFFFFExtras|r, /fbp damage"; +FBLocale["frFR"].DMG_ON = "Smart Damage |cFF00FF00activé|r."; +FBLocale["frFR"].DMG_OFF = "Smart Damage |cFFFF0000désactivé|r."; + +FBLocale["itIT"].DMG_HEADER = "Smart Damage"; +FBLocale["itIT"].DMG_ENABLED = "Smart Damage"; +FBLocale["itIT"].DMG_ENABLED_TIP = "Cosa fa: quando premi un incantesimo d'attacco su una qualsiasi barra delle azioni (Bongos, barre Blizzard, scorciatoie) e un rango inferiore di quell'incantesimo ucciderebbe comunque il bersaglio, viene lanciato quel rango. Risparmia mana sui bersagli quasi morti. Regole: mai sopra il rango sulla barra, il danno minimo del rango deve coprire la salute rimanente del bersaglio più il margine di sicurezza. Richiede la salute reale del bersaglio: dal server, da MobHealth3 o MobInfo-2, o dalla stima dell'addon appresa combattendo un tipo di mostro. Svantaggi: le stime possono sbagliare, le resistenze parziali riducono il danno e sui mostri sconosciuti non succede nulla finché non si è appreso abbastanza. Disattivato per impostazione predefinita."; +FBLocale["itIT"].DMG_MARGIN = "Margine di sicurezza: |cFFFFFFFF%s %%"; +FBLocale["itIT"].DMG_SRC = "Fonte della salute del bersaglio: |cFFFFFFFF%s"; +FBLocale["itIT"].DMG_SRC_REAL = "il server invia valori reali"; +FBLocale["itIT"].DMG_SRC_MH3 = "MobHealth3"; +FBLocale["itIT"].DMG_SRC_MI2 = "MobInfo-2"; +FBLocale["itIT"].DMG_SRC_EST = "stima propria (%d tipi di mostro appresi)"; +FBLocale["itIT"].DMG_SRC_NONE = "nessun bersaglio"; +FBLocale["itIT"].DMG_SRC_UNKNOWN = "solo percentuale, ancora nulla appreso"; +FBLocale["itIT"].DMG_SPELLS = "Incantesimi: |cFFFFFFFF%s"; +FBLocale["itIT"].DMG_INFO = "La riga del bersaglio qui sopra si aggiorna con il tuo bersaglio attuale. L'apprendimento è automatico: ogni combattimento contro un tipo di mostro migliora la stima per quel tipo."; +FBLocale["itIT"].DMG_STATUS = "Smart Damage: %s, margine %d %%, %s"; +FBLocale["itIT"].DBG_DMG = "Smart Damage: %s -> %s (bersaglio %d PS via %s, danno minimo %d)"; +FBLocale["itIT"].DBG_DMG_KEEP = "Smart Damage: %s mantenuto (bersaglio %d PS via %s)"; +FBLocale["itIT"].DMG_LOADED = "Modulo Smart Damage caricato: sezione nella scheda |cFFFFFFFFExtra|r, /fbp damage"; +FBLocale["itIT"].DMG_ON = "Smart Damage |cFF00FF00attivo|r."; +FBLocale["itIT"].DMG_OFF = "Smart Damage |cFFFF0000disattivato|r."; + +-- ========================================================================== +-- [ Zustand ] +-- ========================================================================== + +FBDmgSpellRanks = {}; -- [Name] = { { rank, id, min, max }, ... } aufsteigend +FBDmgSlotCache = {}; -- [slot] = { name, rank } oder false (kein Zauber) +FBDmgAnchorPct = nil; -- Prozent des Ziels beim letzten Messpunkt +FBDmgAccum = 0; -- Schaden auf das Ziel seit dem Messpunkt +FBDmgTargetName = nil; +FBDmgLastCast = nil; -- { spell, rank, t } fuer das Lernen aus dem Combatlog + +function FBDmg_Cfg() + if (not HealBox.Damage) then FBDmg_ApplyDefaults(); end + return HealBox.Damage; +end + +function FBDmg_ApplyDefaults() + if (not HealBox.Damage) then HealBox.Damage = {}; end + for k, v in pairs(FBDamageDefaults) do + if (HealBox.Damage[k] == nil) then HealBox.Damage[k] = v; end + end + if (not HealBox.MobHP) then HealBox.MobHP = {}; end -- [Name:Stufe] = { ppp, n } + if (not HealBox.DmgMemory) then HealBox.DmgMemory = {}; end -- [Zauber|Rang] = gelernter Mindestschaden + return true; +end + +-- ========================================================================== +-- [ Zauberbuch: Raenge und Schaden ] +-- ========================================================================== + +function FBDmg_ScanSpells() + FBDmgSpellRanks = {}; + local wanted = {}; + for _, n in ipairs(FBDamageSpells[FBClass] or {}) do wanted[n] = true; end + local i = 1; + while true do + local name, rank = GetSpellName(i, BOOKTYPE_SPELL); + if (not name) then break; end + if (wanted[name]) then + local txt = FBPredict_TooltipText(i) or ""; + local lo, hi = FBPredict_FindAmountRange(txt); + if (not lo) then + lo = FBPredict_FindAmountSingle(txt); + hi = lo; + end + local isDamage = (string.find(txt, "[Dd]amage") or string.find(txt, "[Ss]chaden")) ~= nil; + if (lo and isDamage) then + if (not FBDmgSpellRanks[name]) then FBDmgSpellRanks[name] = {}; end + table.insert(FBDmgSpellRanks[name], { rank = rank or "", id = i, min = lo, max = hi }); + end + end + i = i + 1; + end + FBDmgSlotCache = {}; +end + +-- Mindestschaden eines Rangs: gelernt (Treffer ohne Teilwiderstand), sonst Tooltip +function FBDmg_MinDamage(spell, entry) + local learned = HealBox.DmgMemory and HealBox.DmgMemory[spell.."|"..entry.rank]; + if (learned and learned > entry.min) then return learned; end + return entry.min; +end + +-- ========================================================================== +-- [ Leben des Ziels ] +-- ========================================================================== + +function FBDmg_MobKey(unit) + local name = UnitName(unit); + if (not name) then return nil; end + local lvl = UnitLevel and UnitLevel(unit) or 0; + return name..":"..tostring(lvl or 0); +end + +-- Sendet der Server echte Werte fuer diese Einheit? +function FBDmg_HasRealValues(unit) + local max = UnitHealthMax(unit); + return (max and max ~= 100 and max > 0); +end + +-- Liefert cur, max, source oder nil. cur ist bei Schaetzung die Obergrenze. +function FBDmg_TargetHP(unit) + unit = unit or "target"; + if (not UnitExists(unit)) then return nil; end + if (FBDmg_HasRealValues(unit)) then + return UnitHealth(unit), UnitHealthMax(unit), "real"; + end + -- MobHealth3 + if (MobHealth3 and MobHealth3.GetUnitHealth) then + local ok, cur, max, found = pcall(MobHealth3.GetUnitHealth, MobHealth3, unit); + if (ok and found and cur and max and max > 100) then return cur, max, "mh3"; end + end + -- MobInfo-2 (nur fuer das aktuelle Ziel) + if (unit == "target" and MobHealth_GetTargetCurHP and MobHealth_GetTargetMaxHP) then + local ok, cur = pcall(MobHealth_GetTargetCurHP); + local ok2, max = pcall(MobHealth_GetTargetMaxHP); + if (ok and ok2 and cur and max and max > 100) then return cur, max, "mi2"; end + end + -- eigene Schaetzung: Prozent-Obergrenze x Lebenspunkte je Prozent + local key = FBDmg_MobKey(unit); + local est = key and HealBox.MobHP and HealBox.MobHP[key]; + if (est and est.ppp and est.ppp > 0) then + local pct = UnitHealth(unit) or 0; + return math.ceil((pct + 1) * est.ppp), math.ceil(100 * est.ppp), "est"; + end + return nil; +end + +-- [ Lernen: Lebenspunkte je Prozent ] --------------------------------------- + +function FBDmg_ResetTarget() + FBDmgTargetName = nil; + FBDmgAnchorPct = nil; + FBDmgAccum = 0; + if (UnitExists("target") and UnitCanAttack and UnitCanAttack("player", "target") and not FBDmg_HasRealValues("target")) then + FBDmgTargetName = UnitName("target"); + FBDmgAnchorPct = UnitHealth("target"); + end + FBDmg_UpdateSourceText(); +end + +-- Schaden auf das Ziel vermerken (aus dem Combatlog) +function FBDmg_NoteDamage(victim, amount) + if (not FBDmgTargetName) or (victim ~= FBDmgTargetName) then return; end + FBDmgAccum = FBDmgAccum + (amount or 0); +end + +-- UNIT_HEALTH des Ziels: Prozent gefallen -> Messung +function FBDmg_OnTargetHealth() + if (not FBDmgTargetName) or (not FBDmgAnchorPct) then return; end + if (FBDmg_HasRealValues("target")) then return; end + local pct = UnitHealth("target") or 0; + local drop = FBDmgAnchorPct - pct; + if (drop >= FBDMG_MIN_DROP and FBDmgAccum > 0) then + local ppp = FBDmgAccum / drop; + local key = FBDmg_MobKey("target"); + if (key) then + if (not HealBox.MobHP) then HealBox.MobHP = {}; end + local e = HealBox.MobHP[key]; + if (not e) then + HealBox.MobHP[key] = { ppp = ppp, n = 1 }; + else + -- Maximum: Fehler nur nach oben (fremder, nicht gezaehlter Schaden + -- wuerde die Schaetzung sonst nach unten ziehen) + if (ppp > e.ppp) then e.ppp = ppp; end + e.n = (e.n or 0) + 1; + end + end + FBDmgAnchorPct = pct; + FBDmgAccum = 0; + FBDmg_UpdateSourceText(); + elseif (pct > FBDmgAnchorPct) then + -- Ziel wurde geheilt: neu ansetzen + FBDmgAnchorPct = pct; + FBDmgAccum = 0; + end +end + +-- [ Combatlog ] -------------------------------------------------------------- + +-- Schadenszeilen: Opfer und Betrag herausziehen (englischer Client, wie das +-- ganze Addon). "(N resisted)" markiert Teilwiderstand. +function FBDmg_ParseLine(msg) + if (not msg) then return nil; end + local _, _, spell, victim, amount; + -- "Your Smite hits Kobold for 95." / "crits" + _, _, spell, victim, amount = string.find(msg, "^Your (.-) hits (.-) for (%d+)"); + if (not spell) then _, _, spell, victim, amount = string.find(msg, "^Your (.-) crits (.-) for (%d+)"); end + if (spell) then return victim, tonumber(amount), spell, "self"; end + -- "You hit Kobold for 20." / "You crit" + _, _, victim, amount = string.find(msg, "^You hit (.-) for (%d+)"); + if (not victim) then _, _, victim, amount = string.find(msg, "^You crit (.-) for (%d+)"); end + if (victim) then return victim, tonumber(amount), nil, "self"; end + -- "Kobold suffers 50 Shadow damage from your Shadow Word: Pain." + _, _, victim, amount = string.find(msg, "^(.-) suffers (%d+) .- damage from your"); + if (victim) then return victim, tonumber(amount), nil, "self"; end + -- "Bob's Fireball hits Kobold for 200." / crits (Gruppe, Begleiter) + _, _, victim, amount = string.find(msg, "^.-'s .- hits (.-) for (%d+)"); + if (not victim) then _, _, victim, amount = string.find(msg, "^.-'s .- crits (.-) for (%d+)"); end + if (victim) then return victim, tonumber(amount), nil, "other"; end + -- "Bob hits Kobold for 50." / crits (Nahkampf Gruppe, Begleiter) + _, _, victim, amount = string.find(msg, "^.- hits (.-) for (%d+)"); + if (not victim) then _, _, victim, amount = string.find(msg, "^.- crits (.-) for (%d+)"); end + if (victim) then return victim, tonumber(amount), nil, "other"; end + return nil; +end + +function FBDmg_OnCombatLog(msg) + local victim, amount, spell, who = FBDmg_ParseLine(msg); + if (not victim) then return; end + FBDmg_NoteDamage(victim, amount); + -- Mindestschaden lernen: eigener Zauber, kein Crit, kein Teilwiderstand + if (who == "self" and spell and FBDmgLastCast and FBDmgLastCast.spell == spell + and (GetTime() - FBDmgLastCast.t) <= FBDMG_LASTCAST_SEC + and not string.find(msg, "crits") and not string.find(msg, "resisted")) then + local key = spell.."|"..(FBDmgLastCast.rank or ""); + if (not HealBox.DmgMemory) then HealBox.DmgMemory = {}; end + local old = HealBox.DmgMemory[key]; + if (not old) or (amount < old) then + -- kleinster beobachteter Volltreffer = sicherer Mindestschaden + HealBox.DmgMemory[key] = amount; + end + end +end + +-- ========================================================================== +-- [ Rangwahl beim Klick ] +-- ========================================================================== + +-- Zauber in einem Aktionsslot: name, rank oder nil (Makro, Gegenstand) +function FBDmg_SlotSpell(slot) + local c = FBDmgSlotCache[slot]; + if (c ~= nil) then + if (c == false) then return nil; end + return c.name, c.rank; + end + if (not FBPredictTip) or (not FBPredictTip.SetAction) then return nil; end + FBPredictTip:SetOwner(UIParent, "ANCHOR_NONE"); + FBPredictTip:ClearLines(); + FBPredictTip:SetAction(slot); + local l = getglobal("FBHealBoxScanTipTextLeft1"); + local r = getglobal("FBHealBoxScanTipTextRight1"); + local name = l and l:GetText(); + local rank = r and r:GetText(); + if (name and FBDmgSpellRanks[name]) then + FBDmgSlotCache[slot] = { name = name, rank = rank or "" }; + return name, rank or ""; + end + FBDmgSlotCache[slot] = false; + return nil; +end + +-- Liefert den zu wirkenden Cast-String, wenn abgerangt wird, sonst nil +function FBDmg_Choose(spell, rank) + local cfg = FBDmg_Cfg(); + local ranks = FBDmgSpellRanks[spell]; + if (not ranks) or (table.getn(ranks) < 2) then return nil; end + if (not UnitExists("target")) or (UnitCanAttack and not UnitCanAttack("player", "target")) then return nil; end + if (UnitIsDead and UnitIsDead("target")) then return nil; end + local cur, max, source = FBDmg_TargetHP("target"); + if (not cur) or (cur <= 0) then return nil; end + local need = cur * (1 + (cfg.Margin or 20) / 100); + + local assignedIdx = nil; + for i, e in ipairs(ranks) do + if (e.rank == rank) then assignedIdx = i; end + end + if (not assignedIdx) then assignedIdx = table.getn(ranks); end + + for i = 1, assignedIdx do + local e = ranks[i]; + if (FBDmg_MinDamage(spell, e) >= need) then + if (i == assignedIdx) then + if (FBPredictDebug) then + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..format(FBT("DBG_DMG_KEEP"), spell.."("..rank..")", cur, source)); + end + return nil; + end + local chosen = spell; + if (e.rank ~= "") then chosen = spell.."("..e.rank..")"; end + if (FBPredictDebug) then + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..format(FBT("DBG_DMG"), spell.."("..rank..")", chosen, cur, source, FBDmg_MinDamage(spell, e))); + end + return chosen, e.rank; + end + end + return nil; +end + +-- Vor-Hook auf UseAction. true, wenn das Modul selbst gecastet hat. +function FBDmg_TryDownrank(slot, checkCursor, onSelf) + if (FBDmg_Cfg().Enabled ~= 1) or onSelf then return false; end + if ((CursorHasSpell and CursorHasSpell()) or (CursorHasItem and CursorHasItem())) then return false; end + local name, rank = FBDmg_SlotSpell(slot); + if (not name) then return false; end + local chosen, chosenRank = FBDmg_Choose(name, rank); + FBDmgLastCast = { spell = name, rank = (chosenRank or rank), t = GetTime() }; + if (not chosen) then return false; end + CastSpellByName(chosen); + return true; +end + +function FBDmg_HookUseAction() + if (FBHealBox_UseActionHooked) or (not UseAction) then return; end + FBHealBox_UseActionHooked = true; + local origUseAction = UseAction; + UseAction = function(slot, checkCursor, onSelf) + if (FBDmg_TryDownrank(slot, checkCursor, onSelf)) then return; end + origUseAction(slot, checkCursor, onSelf); + end +end + +-- ========================================================================== +-- [ Events ] +-- ========================================================================== + +FBDmgFrame = CreateFrame("Frame", "FBHealBoxDamageFrame", UIParent); +FBDmgFrame:RegisterEvent("PLAYER_ENTERING_WORLD"); +FBDmgFrame:RegisterEvent("SPELLS_CHANGED"); +FBDmgFrame:RegisterEvent("ACTIONBAR_SLOT_CHANGED"); +FBDmgFrame:RegisterEvent("PLAYER_TARGET_CHANGED"); +FBDmgFrame:RegisterEvent("UNIT_HEALTH"); +for _, ev in ipairs({ + "CHAT_MSG_SPELL_SELF_DAMAGE", "CHAT_MSG_COMBAT_SELF_HITS", + "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", + "CHAT_MSG_SPELL_PARTY_DAMAGE", "CHAT_MSG_COMBAT_PARTY_HITS", + "CHAT_MSG_SPELL_PET_DAMAGE", "CHAT_MSG_COMBAT_PET_HITS", +}) do FBDmgFrame:RegisterEvent(ev); end + +FBDmgFrame:SetScript("OnEvent", function() + if (event == "PLAYER_ENTERING_WORLD" or event == "SPELLS_CHANGED") then + FBDmg_ScanSpells(); + FBDmg_UpdateSpellText(); + FBDmg_ResetTarget(); + elseif (event == "ACTIONBAR_SLOT_CHANGED") then + if (arg1) then FBDmgSlotCache[arg1] = nil; else FBDmgSlotCache = {}; end + elseif (event == "PLAYER_TARGET_CHANGED") then + FBDmg_ResetTarget(); + elseif (event == "UNIT_HEALTH") then + if (arg1 == "target") then FBDmg_OnTargetHealth(); FBDmg_UpdateSourceText(); end + else + FBDmg_OnCombatLog(arg1); + end +end); + +-- ========================================================================== +-- [ Optionen: Abschnitt im Reiter "Extras" ] +-- ========================================================================== + +function FBDmg_SourceText() + if (not UnitExists("target")) then return FBT("DMG_SRC_NONE"); end + local cur, max, source = FBDmg_TargetHP("target"); + if (source == "real") then return FBT("DMG_SRC_REAL"); end + if (source == "mh3") then return FBT("DMG_SRC_MH3"); end + if (source == "mi2") then return FBT("DMG_SRC_MI2"); end + if (source == "est") then + local n = 0; + for _ in pairs(HealBox.MobHP or {}) do n = n + 1; end + return format(FBT("DMG_SRC_EST"), n); + end + return FBT("DMG_SRC_UNKNOWN"); +end + +function FBDmg_UpdateSourceText() + if (FBDmgSourceText) then FBDmgSourceText:SetText(format(FBT("DMG_SRC"), FBDmg_SourceText())); end +end + +function FBDmg_UpdateSpellText() + if (not FBDmgSpellText) then return; end + local list = {}; + for _, n in ipairs(FBDamageSpells[FBClass] or {}) do + if (FBDmgSpellRanks[n]) then table.insert(list, n.." ("..table.getn(FBDmgSpellRanks[n])..")"); end + end + local shown = "-"; + if (table.getn(list) > 0) then shown = table.concat(list, ", "); end + FBDmgSpellText:SetText(format(FBT("DMG_SPELLS"), shown)); +end + +function FBDmg_UpdateMarginText() + if (FBDmgMarginSlider and FBDmgMarginSlider.Text) then + FBDmgMarginSlider.Text:SetText(format(FBT("DMG_MARGIN"), math.floor(FBDmgMarginSlider:GetValue() + 0.5))); + end +end + +function FBDmg_BuildOptions() + local tab = FBHealBox_FindOptionsTab("TAB_TICKER"); + if (not tab) then return; end + local y = FBOPT_CONTENT_Y - 232; -- unter dem Ticker-Abschnitt + + FBDmgHeader = tab:CreateFontString(nil, "OVERLAY", "GameFontNormal"); + FBDmgHeader:SetPoint("TOPLEFT", tab, "TOPLEFT", 35, y); + FBDmgHeader:SetText(FBT("DMG_HEADER")); + + FBDmgEnabledCheck = FBHealBox_CreateCheck("FBHealBoxDmgEnabledCheck", tab, 34, y - 20, "DMG_ENABLED", "DMG_ENABLED_TIP", function() + FBDmg_Cfg().Enabled = FBDmgEnabledCheck:GetChecked() and 1 or 0; + end); + FBDmgEnabledCheck:SetChecked(nil); + + FBDmgMarginSlider = CreateFrame("Slider", "FBDmgMarginSlider", tab, "OptionsSliderTemplate"); + FBDmgMarginSlider:SetWidth(170); + FBDmgMarginSlider:SetHeight(16); + FBDmgMarginSlider:SetPoint("TOPLEFT", 268, y - 40); + FBDmgMarginSlider:SetMinMaxValues(0, 50); + FBDmgMarginSlider:SetValueStep(5); + FBDmgMarginSlider:SetValue(FBDmg_Cfg().Margin or 20); + FBDmgMarginSlider.Text = FBDmgMarginSlider:CreateFontString(nil, "BACKGROUND", "GameFontNormal"); + FBDmgMarginSlider.Text:SetPoint("CENTER", 0, 15); + getglobal("FBDmgMarginSliderLow"):SetText("0"); + getglobal("FBDmgMarginSliderHigh"):SetText("50"); + FBDmgMarginSlider:SetScript("OnValueChanged", function() + FBDmg_Cfg().Margin = math.floor(FBDmgMarginSlider:GetValue() + 0.5); + FBDmg_UpdateMarginText(); + end); + FBDmg_UpdateMarginText(); + + FBDmgSourceText = tab:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); + FBDmgSourceText:SetPoint("TOPLEFT", tab, "TOPLEFT", 40, y - 80); + FBDmgSourceText:SetWidth(390); + FBDmgSourceText:SetJustifyH("LEFT"); + FBDmgSpellText = tab:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); + FBDmgSpellText:SetPoint("TOPLEFT", tab, "TOPLEFT", 40, y - 96); + FBDmgSpellText:SetWidth(390); + FBDmgSpellText:SetJustifyH("LEFT"); + FBDmgInfoText = tab:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall"); + FBDmgInfoText:SetPoint("TOPLEFT", tab, "TOPLEFT", 35, y - 120); + FBDmgInfoText:SetWidth(395); + FBDmgInfoText:SetJustifyH("LEFT"); + FBDmgInfoText:SetText(FBT("DMG_INFO")); + + FBDmg_UpdateSourceText(); + FBDmg_UpdateSpellText(); + FBDmg_SyncOptions(); +end + +function FBDmg_SyncOptions() + local cfg = FBDmg_Cfg(); + if (FBDmgEnabledCheck) then FBDmgEnabledCheck:SetChecked(cfg.Enabled == 1); end + if (FBDmgMarginSlider) then FBDmgMarginSlider:SetValue(cfg.Margin or 20); end + return true; +end + +function FBDmg_ApplyLocale() + if (FBDmgHeader) then FBDmgHeader:SetText(FBT("DMG_HEADER")); end + if (FBDmgEnabledCheck) then FBDmgEnabledCheck.Text:SetText(FBT("DMG_ENABLED")); FBDmgEnabledCheck.tooltipText = FBT("DMG_ENABLED_TIP"); end + if (FBDmgInfoText) then FBDmgInfoText:SetText(FBT("DMG_INFO")); end + FBDmg_UpdateMarginText(); + FBDmg_UpdateSourceText(); + FBDmg_UpdateSpellText(); + return true; +end + +-- ========================================================================== +-- [ Anbindung an den Kern ] +-- ========================================================================== + +FBHealBox_RegisterHook("Loaded", function() + DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..FBT("DMG_LOADED")); + return true; +end); +FBHealBox_RegisterHook("Defaults", function() return FBDmg_ApplyDefaults(); end); +FBHealBox_RegisterHook("SyncOptions", function() return FBDmg_SyncOptions(); end); +FBHealBox_RegisterHook("ApplyLocale", function() return FBDmg_ApplyLocale(); end); +FBHealBox_RegisterHook("Status", function() + local cfg = FBDmg_Cfg(); + local state = FBT("FBP_STATE_OFF"); + if (cfg.Enabled == 1) then state = FBT("FBP_STATE_ON"); end + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..format(FBT("DMG_STATUS"), state, cfg.Margin or 20, FBDmg_SourceText())); + return true; +end); +FBHealBox_RegisterHook("Slash", function(msg) + if (msg == "damage" or msg == "dmg") then + local cfg = FBDmg_Cfg(); + if (cfg.Enabled == 1) then cfg.Enabled = 0; else cfg.Enabled = 1; end + FBDmg_SyncOptions(); + local key = "DMG_OFF"; + if (cfg.Enabled == 1) then key = "DMG_ON"; end + DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..FBT(key)); + return true; + end + return false; +end); + +FBDmg_ApplyDefaults(); +FBDmg_BuildOptions(); +FBDmg_HookUseAction(); diff --git a/FBHealBox_Raid.lua b/FBHealBox_Raid.lua index ba6a5e0..d556bfe 100644 --- a/FBHealBox_Raid.lua +++ b/FBHealBox_Raid.lua @@ -33,6 +33,10 @@ FBRAID_MANA_H = 3; -- px Manastreifen in der Zelle FBRAID_LOS_SIZE = 10; -- px Sichtlinien-Auge in der Zelle FBRAID_HEADER_H = 12; -- px Gruppenkopf FBRAID_TITLE_H = 14; -- px Titelleiste (Griff zum Verschieben) +FBRAID_BUFFICON_SIZE = 6; -- px Buff-Icons aussen links an der Zelle +FBRAID_BUFFICON_ROWS = 3; -- Stapelhoehe (3 x 4 = 12 Plaetze) +FBRAID_BUFFICON_COLS = 4; -- so viele Spalten bekommen Platz zwischen den Gruppen +FBRAID_BUFFICON_MAX = 12; -- Vorgaben aller Raid-Einstellungen. Leben in HealBox.Raid. FBRaidDefaults = { @@ -52,6 +56,7 @@ FBRaidDefaults = { HPText = "percent",-- none | percent | deficit HideEmpty = 1, -- leere Gruppen ausblenden ShowTitle = 1, -- Titelleiste anzeigen + BuffIcons = 1, -- Buff-Icons links an den Zellen (aus: kein Platz reserviert) PosX = nil, PosY = nil, -- Position der linken oberen Ecke (Bildschirmpixel) }; @@ -74,6 +79,8 @@ FBLocale["enUS"].RAID_HIDEEMPTY = "Hide empty groups"; FBLocale["enUS"].RAID_HIDEEMPTY_TIP = "Groups without members take no space. A 20-player raid then fills a single row."; FBLocale["enUS"].RAID_SHOWTITLE = "Title bar"; FBLocale["enUS"].RAID_SHOWTITLE_TIP = "A thin title bar above the grid that can be dragged. Without it, hold Shift and drag any cell."; +FBLocale["enUS"].RAID_BUFFICONS = "Show buffs"; +FBLocale["enUS"].RAID_BUFFICONS_TIP = "Buff icons with clock on the left of each cell, as on the party plates. Off: no icons and no room reserved for them, the groups move closer together."; FBLocale["enUS"].RAID_HPTEXT = "HP text"; FBLocale["enUS"].RAID_HPTEXT_TIP = "What the right side of a cell shows: nothing, the health percentage, or the missing health (deficit)."; FBLocale["enUS"].RAID_HP_NONE = "none"; @@ -114,6 +121,8 @@ FBLocale["deDE"].RAID_HIDEEMPTY = "Leere Gruppen ausblenden"; FBLocale["deDE"].RAID_HIDEEMPTY_TIP = "Gruppen ohne Mitglieder brauchen keinen Platz. Ein 20er-Raid fuellt dann eine einzige Zeile."; FBLocale["deDE"].RAID_SHOWTITLE = "Titelleiste"; FBLocale["deDE"].RAID_SHOWTITLE_TIP = "Eine schmale Titelleiste ueber dem Raster, an der man ziehen kann. Ohne sie: Shift halten und eine beliebige Zelle ziehen."; +FBLocale["deDE"].RAID_BUFFICONS = "Buffs anzeigen"; +FBLocale["deDE"].RAID_BUFFICONS_TIP = "Buff-Icons mit Uhr links an jeder Zelle, wie auf den Gruppenplaketten. Aus: keine Icons und kein reservierter Platz, die Gruppen ruecken enger zusammen."; FBLocale["deDE"].RAID_HPTEXT = "HP-Text"; FBLocale["deDE"].RAID_HPTEXT_TIP = "Was die rechte Seite einer Zelle zeigt: nichts, die Lebensprozente oder die fehlenden Lebenspunkte (Defizit)."; FBLocale["deDE"].RAID_HP_NONE = "keiner"; @@ -137,6 +146,134 @@ FBLocale["deDE"].RAID_INFO = "Die Mini-Buttons nutzen die Zauber von But FBLocale["deDE"].RAID_MOVE_TIP = "Ziehen verschiebt das Raid-Raster."; FBLocale["deDE"].FBP_RAID = "Raidmodus: %s, %d Mitglieder in %d Gruppen"; FBLocale["deDE"].FBP_RAID_CMDS = "Raid: /fbp raid (an/aus), /fbp raidtest 20|40|off"; + +-- Weitere Sprachen (Spanisch, Franzoesisch, Italienisch) + +FBLocale["esES"].TAB_RAID = "Modo banda"; +FBLocale["esES"].RAID_TITLE = "Banda"; +FBLocale["esES"].RAID_ENABLED = "Modo banda"; +FBLocale["esES"].RAID_ENABLED_TIP = "Interruptor principal de la rejilla de banda compacta: una celda por miembro, agrupadas por grupo de banda, con minibotones de curación. La rejilla aparece sola cuando tu banda tiene al menos el número de jugadores fijado abajo; las bandas más pequeñas conservan la vista de grupo."; +FBLocale["esES"].RAID_MINPLAYERS = "Vista de banda a partir de |cFFFFFFFF%s|r jugadores"; +FBLocale["esES"].RAID_HIDEPARTY = "Ocultar placas de grupo"; +FBLocale["esES"].RAID_HIDEPARTY_TIP = "Mientras se muestra la rejilla de banda, las cinco placas de grupo se ocultan. Desactívalo para conservar ambas."; +FBLocale["esES"].RAID_HEADERS = "Cabeceras de grupo"; +FBLocale["esES"].RAID_HEADERS_TIP = "Un pequeño número de grupo sobre cada bloque."; +FBLocale["esES"].RAID_MANABAR = "Franja de maná"; +FBLocale["esES"].RAID_MANABAR_TIP = "Una franja de maná de 3 px en la parte inferior de cada celda, solo para usuarios de maná."; +FBLocale["esES"].RAID_HIDEEMPTY = "Ocultar grupos vacíos"; +FBLocale["esES"].RAID_HIDEEMPTY_TIP = "Los grupos sin miembros no ocupan espacio. Una banda de 20 llena entonces una sola fila."; +FBLocale["esES"].RAID_SHOWTITLE = "Barra de título"; +FBLocale["esES"].RAID_SHOWTITLE_TIP = "Una fina barra de título sobre la rejilla que se puede arrastrar. Sin ella, mantén Mayús y arrastra cualquier celda."; +FBLocale["esES"].RAID_BUFFICONS = "Mostrar beneficios"; +FBLocale["esES"].RAID_BUFFICONS_TIP = "Iconos de beneficios con reloj a la izquierda de cada celda, como en las placas de grupo. Desactivado: sin iconos ni espacio reservado, los grupos se juntan más."; +FBLocale["esES"].RAID_HPTEXT = "Texto de vida"; +FBLocale["esES"].RAID_HPTEXT_TIP = "Qué muestra el lado derecho de una celda: nada, el porcentaje de vida o la vida que falta (déficit)."; +FBLocale["esES"].RAID_HP_NONE = "nada"; +FBLocale["esES"].RAID_HP_PERCENT = "porcentaje"; +FBLocale["esES"].RAID_HP_DEFICIT = "déficit"; +FBLocale["esES"].RAID_GROUPSPERROW = "Grupos por fila: |cFFFFFFFF%s"; +FBLocale["esES"].RAID_SCALE = "Escala de banda: |cFFFFFFFF%s"; +FBLocale["esES"].RAID_CELLW = "Ancho de celda: |cFFFFFFFF%s px"; +FBLocale["esES"].RAID_CELLH = "Alto de celda: |cFFFFFFFF%s px"; +FBLocale["esES"].RAID_BUTTONS = "Botones por unidad: |cFFFFFFFF%s"; +FBLocale["esES"].RAID_BTNSIZE = "Tamaño de botón: |cFFFFFFFF%s px"; +FBLocale["esES"].RAID_CELLGAP = "Separación de celdas: |cFFFFFFFF%s px"; +FBLocale["esES"].RAID_GROUPGAP = "Separación de grupos: |cFFFFFFFF%s px"; +FBLocale["esES"].RAID_TEST = "Prueba de banda"; +FBLocale["esES"].RAID_TEST_TIP = "Rellena la rejilla de banda con 20 o 40 jugadores fantasma para organizarla sin banda. No se guarda."; +FBLocale["esES"].RAID_TEST_OFF = "desactivada"; +FBLocale["esES"].RAID_TEST_20 = "20 jugadores"; +FBLocale["esES"].RAID_TEST_40 = "40 jugadores"; +FBLocale["esES"].RAID_TEST_CLICK = "Prueba de banda: sin lanzamiento sobre un jugador fantasma."; +FBLocale["esES"].RAID_INFO = "Los minibotones usan los hechizos de los botones 1 a N de la pestaña Botones, incluidos los del clic derecho."; +FBLocale["esES"].RAID_MOVE_TIP = "Arrastra para mover la rejilla de banda."; +FBLocale["esES"].FBP_RAID = "Modo banda: %s, %d miembros en %d grupos"; +FBLocale["esES"].FBP_RAID_CMDS = "Banda: /fbp raid (alternar), /fbp raidtest 20|40|off"; +FBLocale["esES"].RAID_LOADED = "Módulo de modo banda cargado: pestaña |cFFFFFFFFModo banda|r en las opciones, /fbp raid, /fbp raidtest 20|40"; + +FBLocale["frFR"].TAB_RAID = "Mode raid"; +FBLocale["frFR"].RAID_TITLE = "Raid"; +FBLocale["frFR"].RAID_ENABLED = "Mode raid"; +FBLocale["frFR"].RAID_ENABLED_TIP = "Interrupteur principal de la grille de raid compacte : une cellule par membre, regroupées par groupe de raid, avec des mini-boutons de soins. La grille apparaît d'elle-même dès que votre raid compte au moins le nombre de joueurs réglé ci-dessous ; les raids plus petits gardent l'affichage de groupe."; +FBLocale["frFR"].RAID_MINPLAYERS = "Vue raid à partir de |cFFFFFFFF%s|r joueurs"; +FBLocale["frFR"].RAID_HIDEPARTY = "Masquer les plaques de groupe"; +FBLocale["frFR"].RAID_HIDEPARTY_TIP = "Tant que la grille de raid est affichée, les cinq plaques de groupe sont masquées. Désactivez pour garder les deux."; +FBLocale["frFR"].RAID_HEADERS = "En-têtes de groupe"; +FBLocale["frFR"].RAID_HEADERS_TIP = "Un petit numéro de groupe au-dessus de chaque bloc."; +FBLocale["frFR"].RAID_MANABAR = "Bande de mana"; +FBLocale["frFR"].RAID_MANABAR_TIP = "Une bande de mana de 3 px en bas de chaque cellule, uniquement pour les utilisateurs de mana."; +FBLocale["frFR"].RAID_HIDEEMPTY = "Masquer les groupes vides"; +FBLocale["frFR"].RAID_HIDEEMPTY_TIP = "Les groupes sans membres ne prennent pas de place. Un raid de 20 tient alors sur une seule ligne."; +FBLocale["frFR"].RAID_SHOWTITLE = "Barre de titre"; +FBLocale["frFR"].RAID_SHOWTITLE_TIP = "Une fine barre de titre au-dessus de la grille, que l'on peut faire glisser. Sans elle, maintenez Maj et faites glisser n'importe quelle cellule."; +FBLocale["frFR"].RAID_BUFFICONS = "Afficher les buffs"; +FBLocale["frFR"].RAID_BUFFICONS_TIP = "Icônes de buffs avec horloge à gauche de chaque cellule, comme sur les plaques de groupe. Désactivé : pas d'icônes ni d'espace réservé, les groupes se rapprochent."; +FBLocale["frFR"].RAID_HPTEXT = "Texte de vie"; +FBLocale["frFR"].RAID_HPTEXT_TIP = "Ce qu'affiche le côté droit d'une cellule : rien, le pourcentage de vie ou la vie manquante (déficit)."; +FBLocale["frFR"].RAID_HP_NONE = "rien"; +FBLocale["frFR"].RAID_HP_PERCENT = "pourcentage"; +FBLocale["frFR"].RAID_HP_DEFICIT = "déficit"; +FBLocale["frFR"].RAID_GROUPSPERROW = "Groupes par ligne : |cFFFFFFFF%s"; +FBLocale["frFR"].RAID_SCALE = "Échelle du raid : |cFFFFFFFF%s"; +FBLocale["frFR"].RAID_CELLW = "Largeur de cellule : |cFFFFFFFF%s px"; +FBLocale["frFR"].RAID_CELLH = "Hauteur de cellule : |cFFFFFFFF%s px"; +FBLocale["frFR"].RAID_BUTTONS = "Boutons par unité : |cFFFFFFFF%s"; +FBLocale["frFR"].RAID_BTNSIZE = "Taille des boutons : |cFFFFFFFF%s px"; +FBLocale["frFR"].RAID_CELLGAP = "Espacement des cellules : |cFFFFFFFF%s px"; +FBLocale["frFR"].RAID_GROUPGAP = "Espacement des groupes : |cFFFFFFFF%s px"; +FBLocale["frFR"].RAID_TEST = "Test de raid"; +FBLocale["frFR"].RAID_TEST_TIP = "Remplit la grille de raid avec 20 ou 40 joueurs fantômes pour la disposer sans raid. Non sauvegardé."; +FBLocale["frFR"].RAID_TEST_OFF = "désactivé"; +FBLocale["frFR"].RAID_TEST_20 = "20 joueurs"; +FBLocale["frFR"].RAID_TEST_40 = "40 joueurs"; +FBLocale["frFR"].RAID_TEST_CLICK = "Test de raid : aucun lancement sur un joueur fantôme."; +FBLocale["frFR"].RAID_INFO = "Les mini-boutons utilisent les sorts des boutons 1 à N de l'onglet Boutons, sorts du clic droit compris."; +FBLocale["frFR"].RAID_MOVE_TIP = "Faites glisser pour déplacer la grille de raid."; +FBLocale["frFR"].FBP_RAID = "Mode raid : %s, %d membres dans %d groupes"; +FBLocale["frFR"].FBP_RAID_CMDS = "Raid : /fbp raid (basculer), /fbp raidtest 20|40|off"; +FBLocale["frFR"].RAID_LOADED = "Module mode raid chargé : onglet |cFFFFFFFFMode raid|r dans les options, /fbp raid, /fbp raidtest 20|40"; + +FBLocale["itIT"].TAB_RAID = "Incursione"; +FBLocale["itIT"].RAID_TITLE = "Incursione"; +FBLocale["itIT"].RAID_ENABLED = "Modalità incursione"; +FBLocale["itIT"].RAID_ENABLED_TIP = "Interruttore principale della griglia compatta d'incursione: una cella per membro, raggruppate per gruppo, con mini pulsanti di cura. La griglia compare da sola quando l'incursione ha almeno il numero di giocatori impostato sotto; le incursioni più piccole mantengono la vista di gruppo."; +FBLocale["itIT"].RAID_MINPLAYERS = "Vista incursione da |cFFFFFFFF%s|r giocatori"; +FBLocale["itIT"].RAID_HIDEPARTY = "Nascondi targhette di gruppo"; +FBLocale["itIT"].RAID_HIDEPARTY_TIP = "Finché la griglia d'incursione è visibile, le cinque targhette di gruppo sono nascoste. Disattiva per tenerle entrambe."; +FBLocale["itIT"].RAID_HEADERS = "Intestazioni di gruppo"; +FBLocale["itIT"].RAID_HEADERS_TIP = "Un piccolo numero di gruppo sopra ogni blocco."; +FBLocale["itIT"].RAID_MANABAR = "Striscia del mana"; +FBLocale["itIT"].RAID_MANABAR_TIP = "Una striscia del mana di 3 px in fondo a ogni cella, solo per chi usa mana."; +FBLocale["itIT"].RAID_HIDEEMPTY = "Nascondi gruppi vuoti"; +FBLocale["itIT"].RAID_HIDEEMPTY_TIP = "I gruppi senza membri non occupano spazio. Un'incursione da 20 riempie allora una sola riga."; +FBLocale["itIT"].RAID_SHOWTITLE = "Barra del titolo"; +FBLocale["itIT"].RAID_SHOWTITLE_TIP = "Una sottile barra del titolo sopra la griglia che si può trascinare. Senza, tieni premuto Maiusc e trascina una cella qualsiasi."; +FBLocale["itIT"].RAID_BUFFICONS = "Mostra benefici"; +FBLocale["itIT"].RAID_BUFFICONS_TIP = "Icone dei benefici con orologio a sinistra di ogni cella, come sulle targhette di gruppo. Disattivato: niente icone né spazio riservato, i gruppi si avvicinano."; +FBLocale["itIT"].RAID_HPTEXT = "Testo salute"; +FBLocale["itIT"].RAID_HPTEXT_TIP = "Cosa mostra il lato destro di una cella: niente, la percentuale di salute o la salute mancante (deficit)."; +FBLocale["itIT"].RAID_HP_NONE = "niente"; +FBLocale["itIT"].RAID_HP_PERCENT = "percentuale"; +FBLocale["itIT"].RAID_HP_DEFICIT = "deficit"; +FBLocale["itIT"].RAID_GROUPSPERROW = "Gruppi per riga: |cFFFFFFFF%s"; +FBLocale["itIT"].RAID_SCALE = "Scala dell'incursione: |cFFFFFFFF%s"; +FBLocale["itIT"].RAID_CELLW = "Larghezza cella: |cFFFFFFFF%s px"; +FBLocale["itIT"].RAID_CELLH = "Altezza cella: |cFFFFFFFF%s px"; +FBLocale["itIT"].RAID_BUTTONS = "Pulsanti per unità: |cFFFFFFFF%s"; +FBLocale["itIT"].RAID_BTNSIZE = "Dimensione pulsanti: |cFFFFFFFF%s px"; +FBLocale["itIT"].RAID_CELLGAP = "Spaziatura celle: |cFFFFFFFF%s px"; +FBLocale["itIT"].RAID_GROUPGAP = "Spaziatura gruppi: |cFFFFFFFF%s px"; +FBLocale["itIT"].RAID_TEST = "Test incursione"; +FBLocale["itIT"].RAID_TEST_TIP = "Riempie la griglia d'incursione con 20 o 40 giocatori fantasma per sistemarla senza incursione. Non viene salvato."; +FBLocale["itIT"].RAID_TEST_OFF = "disattivato"; +FBLocale["itIT"].RAID_TEST_20 = "20 giocatori"; +FBLocale["itIT"].RAID_TEST_40 = "40 giocatori"; +FBLocale["itIT"].RAID_TEST_CLICK = "Test incursione: nessun lancio su un giocatore fantasma."; +FBLocale["itIT"].RAID_INFO = "I mini pulsanti usano gli incantesimi dei pulsanti da 1 a N della scheda Pulsanti, compresi quelli del clic destro."; +FBLocale["itIT"].RAID_MOVE_TIP = "Trascina per spostare la griglia d'incursione."; +FBLocale["itIT"].FBP_RAID = "Modalità incursione: %s, %d membri in %d gruppi"; +FBLocale["itIT"].FBP_RAID_CMDS = "Incursione: /fbp raid (attiva/disattiva), /fbp raidtest 20|40|off"; +FBLocale["itIT"].RAID_LOADED = "Modulo modalità incursione caricato: scheda |cFFFFFFFFModalità incursione|r nelle opzioni, /fbp raid, /fbp raidtest 20|40"; FBLocale["deDE"].RAID_LOADED = "Raidmodus-Modul geladen: Reiter |cFFFFFFFFRaidmodus|r in den Optionen, /fbp raid, /fbp raidtest 20|40"; -- ========================================================================== @@ -206,11 +343,38 @@ function FBRaid_BuildTestGhosts(count) if (i == 13) then g.state = "offline"; end if (i == 18) then g.state = "ghost"; end if (i == 4 or i == 22) then g.debuff = true; end + if (i == 27) then g.debuffType = "Disease"; end if (i == 9 or i == 31) then g.outOfRange = true; end if (i == 11) then g.los = true; end if (i == 2 or i == 26) then g.buffMissing = true; end if (i == 15) then g.shield = 500; end if (i == 16 or i == 33) then g.inc = 800; end + if (i == 16) then g.aggro = true; g.hotLeft = 7; end + if (i == 15) then g.shieldLeft = 18; end + -- Buff-Icons: jeder dritte Geist traegt Seelenstaerke mit anderer Restzeit, + -- einer davon dazu Goettlichen Willen, einer einen fremden Buff ohne Zeit + if (math.mod(i, 3) == 0) then + local left = math.mod(i * 7, 18) * 100; -- 0..1700 s von 1800 + if (left < 60) then left = 60; end + g.buffs = { { tex = "Interface\\Icons\\Spell_Holy_WordFortitude", left = left, dur = 1800 } }; + if (i == 6) then table.insert(g.buffs, { tex = "Interface\\Icons\\Spell_Holy_DivineSpirit", left = 1500, dur = 1800 }); end + if (i == 9) then g.buffs[1].left = nil; end + if (i == 12) then + -- zwoelf Buffs auf einem Geist (3 x 4), um die Darstellung zu pruefen + g.buffs = { { tex = "Interface\\Icons\\Spell_Holy_WordFortitude", left = 540, dur = 1800 }, + { tex = "Interface\\Icons\\Spell_Holy_DivineSpirit", left = 1500, dur = 1800 }, + { tex = "Interface\\Icons\\Spell_Shadow_AntiShadow", left = 300, dur = 600 }, + { tex = "Interface\\Icons\\Spell_Holy_Excorcism", left = 45, dur = 180 }, + { tex = "Interface\\Icons\\Spell_Nature_Regeneration", dur = 1800 }, + { tex = "Interface\\Icons\\Spell_Magic_MageArmor", left = 1750, dur = 1800 }, + { tex = "Interface\\Icons\\Spell_Holy_SealOfWisdom", left = 200, dur = 300 }, + { tex = "Interface\\Icons\\Spell_Holy_FistOfJustice", left = 100, dur = 300 }, + { tex = "Interface\\Icons\\Spell_Nature_Thorns", left = 500, dur = 600 }, + { tex = "Interface\\Icons\\Spell_Holy_InnerFire", left = 1200, dur = 600 }, + { tex = "Interface\\Icons\\Spell_Holy_SealOfSalvation", left = 900, dur = 1800 }, + { tex = "Interface\\Icons\\Spell_Fire_FireArmor", dur = 1800 } }; + end + end FBRaidTestGhosts[i] = g; end end @@ -287,6 +451,10 @@ end function FBRaid_Dispel(cell) local g = cell.ghost; if (g) then + if (g.debuffType) then + if (FBDispelColors[g.debuffType]) then return g.debuffType; end + return nil; + end if (g.debuff) then for _, dtype in ipairs({ "Magic", "Poison", "Disease", "Curse" }) do if (FBDispelColors[dtype]) then return dtype; end @@ -449,17 +617,24 @@ function FBRaid_CreateCell(g, pos) c.ManaBar.bg:SetTexture(0, 0, 0, FBMANA_BG_ALPHA); c.ManaBar:Hide(); - -- Texte - c.NameText = c:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); + -- Texte liegen auf einer eigenen Ebene ueber den Balken (die Balken sind + -- Kind-Frames mit hoeherer Ebene und wuerden Texte der Zelle verdecken) + c.Overlay = CreateFrame("Frame", nil, c); + c.Overlay:SetAllPoints(c); + c.Overlay:SetFrameStrata("MEDIUM"); + c.Overlay:SetFrameLevel(base + 6); + c.NameText = c.Overlay:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); c.NameText:SetPoint("LEFT", c, "LEFT", 4, 0); + c.NameText:SetHeight(FBNAME_HEIGHT); c.NameText:SetJustifyH("LEFT"); - c.HPText = c:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall"); + c.NameText:SetJustifyV("MIDDLE"); + c.HPText = c.Overlay:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall"); c.HPText:SetPoint("RIGHT", c, "RIGHT", -3, 0); c.HPText:SetJustifyH("RIGHT"); c.HPText:SetTextColor(1, 1, 1, 1); -- Sichtlinie: kleines Auge oben links - c.LOSIcon = c:CreateTexture(nil, "OVERLAY"); + c.LOSIcon = c.Overlay:CreateTexture(nil, "OVERLAY"); c.LOSIcon:SetWidth(FBRAID_LOS_SIZE); c.LOSIcon:SetHeight(FBRAID_LOS_SIZE); c.LOSIcon:SetPoint("TOPLEFT", c, "TOPLEFT", -3, 3); @@ -605,9 +780,9 @@ end -- [ Layout ] -- ========================================================================== --- Breite einer Zelle samt ihrer Buttons +-- Breite einer Zelle samt Buff-Streifen links und Buttons rechts function FBRaid_RowWidth(cfg) - local w = cfg.CellW; + local w = FBRaid_BuffStrip(cfg) + cfg.CellW; if (cfg.Buttons > 0) then w = w + cfg.Buttons * (cfg.ButtonSize + 1); end return w; end @@ -668,12 +843,13 @@ function FBRaid_LayoutAll() col * (rowW + cfg.GroupGap), -(titleH + row * (blockH + cfg.GroupGap))); if (cfg.Headers == 1) then f.header:Show(); else f.header:Hide(); end - -- Zellen im Block + -- Zellen im Block, links davon der Streifen fuer die Buff-Icons + local strip = FBRaid_BuffStrip(cfg); for pos = 1, FBRAID_PER_GROUP do local c = FBRaid_GetCell(g, pos); FBRaid_ApplyCellSize(c, cfg); c:ClearAllPoints(); - c:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -(headH + (pos - 1) * (cfg.CellH + cfg.CellGap))); + c:SetPoint("TOPLEFT", f, "TOPLEFT", strip, -(headH + (pos - 1) * (cfg.CellH + cfg.CellGap))); end f:Show(); col = col + 1; @@ -789,7 +965,14 @@ function FBRaid_UpdateCell(c) end end --- Rahmen: Buff-Wache (orange) oder normal +-- Rahmen: Angegriffener (rot) vor Buff-Wache (orange) vor normal +function FBRaid_ApplyBorder(c) + local col = FBBUFF_NORMAL_COLOR; + if (c.buffMissing) then col = FBBUFF_MISSING_COLOR; end + if (c.underAttack) then col = FBAGGRO_COLOR; end + c:SetBackdropBorderColor(col[1], col[2], col[3], col[4]); +end + function FBRaid_UpdateBuffBorder(c) if (not c) or (not c.unit) then return; end local missing = false; @@ -798,9 +981,96 @@ function FBRaid_UpdateBuffBorder(c) end if (c.buffMissing ~= missing) then c.buffMissing = missing; - local col = FBBUFF_NORMAL_COLOR; - if (missing) then col = FBBUFF_MISSING_COLOR; end - c:SetBackdropBorderColor(col[1], col[2], col[3], col[4]); + FBRaid_ApplyBorder(c); + end +end + +-- Wer wird angegriffen (Hook "Aggro" des Kerns, tt = "targettarget" oder nil) +function FBRaid_CheckAggro(tt) + if (not FBRaid_IsActive()) then return; end + for g = 1, FBRAID_GROUPS do + if (FBRaidCells[g]) then + for pos = 1, FBRAID_PER_GROUP do + local c = FBRaidCells[g][pos]; + if (c and c.unit) then + local flag = false; + if (HealBox.AggroMark == 1 and c:IsShown()) then + if (c.ghost) then flag = (c.ghost.aggro == true); + elseif (tt) then flag = FBHealBox_UnitIsAggro(c.unit, tt); end + end + if (c.underAttack ~= flag) then + c.underAttack = flag; + FBRaid_ApplyBorder(c); + end + end + end + end + end +end + +-- Platz links neben jeder Zelle fuer die Buff-Icons (aussen links) +function FBRaid_BuffStrip(cfg) + if (HealBox.BuffIcons ~= 1) or (cfg.BuffIcons ~= 1) then return 0; end + return -FBBUFFICON_XOFF + FBRAID_BUFFICON_COLS * (FBRAID_BUFFICON_SIZE + FBBUFFICON_GAP); +end + +-- Buff-Icons mit Uhr aussen links an den Zellen (Hook "BuffIcons") +function FBRaid_UpdateBuffIcons() + if (not FBRaid_IsActive()) then return; end + local on = (FBRaid_Cfg().BuffIcons == 1); + for g = 1, FBRAID_GROUPS do + if (FBRaidCells[g]) then + for pos = 1, FBRAID_PER_GROUP do + local c = FBRaidCells[g][pos]; + if (c) then + if (on and c.unit and c:IsShown()) then + FBHealBox_UpdateBuffIcons(c, c.name, c.ghost, FBRAID_BUFFICON_SIZE, FBRAID_BUFFICON_ROWS, FBRAID_BUFFICON_MAX); + else + FBHealBox_UpdateBuffIcons(c, nil, nil, FBRAID_BUFFICON_SIZE, FBRAID_BUFFICON_ROWS, FBRAID_BUFFICON_MAX); + end + end + end + end + end +end + +-- HoT-/Schild-Timer auf den Mini-Buttons (Hook "SpellTimers") +function FBRaid_UpdateSpellTimers(now) + local on = (HealBox.SpellTimers == 1) and FBRaid_IsActive(); + for g = 1, FBRAID_GROUPS do + if (FBRaidCells[g]) then + for pos = 1, FBRAID_PER_GROUP do + local c = FBRaidCells[g][pos]; + if (c) then + local shown = on and c.unit and c:IsShown(); + for i = 1, FBRAID_MAX_BUTTONS do + local b = c.buttons[i]; + if (shown and b:IsShown()) then + local text, color = FBHealBox_SpellTimerFor(b.spellName, c.name, now, c.ghost, i); + if (not text and b.spellNameR) then + text, color = FBHealBox_SpellTimerFor(b.spellNameR, c.name, now, nil, i); + end + FBHealBox_SetButtonTimer(b, text, color); + else + FBHealBox_SetButtonTimer(b, nil); + end + end + end + end + end + end +end + +function FBRaid_UpdateCooldowns() + for g = 1, FBRAID_GROUPS do + if (FBRaidCells[g]) then + for pos = 1, FBRAID_PER_GROUP do + local c = FBRaidCells[g][pos]; + if (c) then + for i = 1, FBRAID_MAX_BUTTONS do FBHealBox_UpdateButtonCooldown(c.buttons[i]); end + end + end + end end end @@ -860,6 +1130,7 @@ function FBRaid_SyncButtons() b.subIcon:Hide(); end end + FBHealBox_UpdateButtonCooldown(b); end end end @@ -1053,9 +1324,14 @@ function FBRaid_BuildOptions() FBRaid_Cfg().ShowTitle = FBRaidTitleCheck:GetChecked() and 1 or 0; FBRaid_LayoutAll(); end); + FBRaidBuffIconsCheck = FBHealBox_CreateCheck("FBHealBoxRaidBuffIconsCheck", tab, 40, y - 90, "RAID_BUFFICONS", "RAID_BUFFICONS_TIP", function() + FBRaid_Cfg().BuffIcons = FBRaidBuffIconsCheck:GetChecked() and 1 or 0; + FBRaid_LayoutAll(); + FBRaid_UpdateBuffIcons(); + end); -- HP-Text: Auswahlknopf mit Kaskadenmenue - FBRaidHPTextBtn = FBHealBox_CreatePickButton("FBHealBoxRaidHPTextBtn", tab, 35, y - 96, 180, 26, 18); + FBRaidHPTextBtn = FBHealBox_CreatePickButton("FBHealBoxRaidHPTextBtn", tab, 35, y - 126, 180, 26, 18); FBRaidHPTextBtn.icon:SetTexture("Interface\\Icons\\INV_Misc_Note_01"); FBRaidHPTextBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT"); @@ -1085,7 +1361,7 @@ function FBRaid_BuildOptions() end); -- Raid-Test: aus / 20 / 40 - FBRaidTestBtn = FBHealBox_CreatePickButton("FBHealBoxRaidTestBtn", tab, 245, y - 96, 180, 26, 18); + FBRaidTestBtn = FBHealBox_CreatePickButton("FBHealBoxRaidTestBtn", tab, 245, y - 126, 180, 26, 18); FBRaidTestBtn.icon:SetTexture("Interface\\Icons\\Spell_Shadow_Twilight"); FBRaidTestBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT"); @@ -1112,7 +1388,7 @@ function FBRaid_BuildOptions() end); -- Schieberegler (Beschriftung sitzt ueber dem Regler) - local sy = y - 150; + local sy = y - 180; FBRaid_CreateSlider("FBRaidGroupsPerRowSlider", tab, 75, sy, "RAID_GROUPSPERROW", "GroupsPerRow", 1, 8, 1, false, FBRaid_LayoutAll); FBRaid_CreateSlider("FBRaidScaleSlider", tab, 260, sy, "RAID_SCALE", "Scale", 0.5, 1.5, 0.1, true, FBRaid_LayoutAll); FBRaid_CreateSlider("FBRaidCellWSlider", tab, 75, sy - 50, "RAID_CELLW", "CellW", 50, 120, 1, false, FBRaid_LayoutAll); @@ -1142,6 +1418,7 @@ function FBRaid_SyncOptions() if (FBRaidManaCheck) then FBRaidManaCheck:SetChecked(cfg.ManaBar == 1); end if (FBRaidHideEmptyCheck) then FBRaidHideEmptyCheck:SetChecked(cfg.HideEmpty == 1); end if (FBRaidTitleCheck) then FBRaidTitleCheck:SetChecked(cfg.ShowTitle == 1); end + if (FBRaidBuffIconsCheck) then FBRaidBuffIconsCheck:SetChecked(cfg.BuffIcons == 1); end for key, s in pairs(FBRaidSliders) do if (cfg[key] ~= nil) then s:SetValue(cfg[key]); end end @@ -1157,6 +1434,7 @@ function FBRaid_ApplyLocale() if (FBRaidManaCheck) then FBRaidManaCheck.Text:SetText(FBT("RAID_MANABAR")); FBRaidManaCheck.tooltipText = FBT("RAID_MANABAR_TIP"); end if (FBRaidHideEmptyCheck) then FBRaidHideEmptyCheck.Text:SetText(FBT("RAID_HIDEEMPTY")); FBRaidHideEmptyCheck.tooltipText = FBT("RAID_HIDEEMPTY_TIP"); end if (FBRaidTitleCheck) then FBRaidTitleCheck.Text:SetText(FBT("RAID_SHOWTITLE")); FBRaidTitleCheck.tooltipText = FBT("RAID_SHOWTITLE_TIP"); end + if (FBRaidBuffIconsCheck) then FBRaidBuffIconsCheck.Text:SetText(FBT("RAID_BUFFICONS")); FBRaidBuffIconsCheck.tooltipText = FBT("RAID_BUFFICONS_TIP"); end if (FBRaidInfoText) then FBRaidInfoText:SetText(FBT("RAID_INFO")); end for _, s in pairs(FBRaidSliders) do FBRaid_SliderText(s); end FBRaid_UpdateHPTextLabel(); @@ -1198,6 +1476,10 @@ FBHealBox_RegisterHook("ButtonsChanged", function() FBRaid_SyncButtons(); return true; end); +FBHealBox_RegisterHook("Aggro", function(tt) FBRaid_CheckAggro(tt); return true; end); +FBHealBox_RegisterHook("SpellTimers", function(now) FBRaid_UpdateSpellTimers(now); return true; end); +FBHealBox_RegisterHook("Cooldowns", function() FBRaid_UpdateCooldowns(); return true; end); +FBHealBox_RegisterHook("BuffIcons", function() FBRaid_UpdateBuffIcons(); return true; end); FBHealBox_RegisterHook("Status", function() local cfg = FBRaid_Cfg(); local state = FBT("FBP_STATE_OFF"); diff --git a/FBHealBox_Ticker.lua b/FBHealBox_Ticker.lua new file mode 100644 index 0000000..0eb5f4c --- /dev/null +++ b/FBHealBox_Ticker.lua @@ -0,0 +1,549 @@ +-- ========================================================================== +-- Heal Box Vanilla: Mana-Ticker (Modul, ab v1.4.3) +-- +-- Ein Funke wandert ueber den Manastreifen der eigenen Plakette (und der +-- eigenen Raid-Zelle) und zeigt den 2-Sekunden-Regenerationstick des +-- Servers. Nach jedem Manaverbrauch laeuft er orange ueber die +-- 5-Sekunden-Regel, verlaengert bis zum ersten Tick nach deren Ablauf, +-- denn erst dort setzt die Regeneration wieder ein. +-- +-- Erkennung ohne Combatlog und ohne SuperWoW: +-- * UNIT_MANA fuer "player": Mana gestiegen = Tick-Kandidat, +-- Mana gesunken = Zauber hat gekostet, Regel startet. +-- * Das Tickraster ist serverseitig fest (2 s). Der letzte erkannte Tick +-- wird gemerkt, weitere werden extrapoliert (auch bei vollem Mana), +-- jeder passende echte Tick synchronisiert neu. +-- * Kandidaten ausserhalb der Toleranz um den erwarteten Zeitpunkt +-- (Manaquelle-Totem, Innervate-Pulse) werden verworfen; erst nach +-- mehreren Fehlschlaegen in Folge wird ein neues Raster angenommen. +-- Grosse Spruenge (Traenke, Runen) zaehlen nie als Tick. +-- +-- Bei vollem Mana ist der Funke aus (nichts zu regenerieren). +-- Haengt sich wie der Raidmodus nur ueber die Hook-Schnittstelle ein. +-- ========================================================================== + +if (FBHealBox_TickerLoaded) then return; end +FBHealBox_TickerLoaded = true; + +FBTICK_PERIOD = 2.0; -- Sek. zwischen zwei Regenerationsticks +FBTICK_FSR = 5.0; -- Sek. Fuenf-Sekunden-Regel +FBTICK_RESYNC_MISS = 3; -- so viele Kandidaten ausserhalb der Toleranz -> neues Raster +FBTICK_POTION_FACT = 4; -- Zuwachs groesser als das Vielfache der Tickgroesse = Trank +FBTICK_POTION_MIN = 300; -- ... und mindestens so viel Mana + +FBTICK_COLOR_TICK = { 1.00, 1.00, 1.00, 0.95 }; -- Funke im Tickzyklus +FBTICK_COLOR_FSR = { 1.00, 0.55, 0.10, 1.00 }; -- Funke waehrend der Regel + +FBTickerDefaults = { + Enabled = 1, -- Ticker an/aus + FSR = 1, -- 5-Sekunden-Regel anzeigen + Tolerance = 0.3, -- Sek. Toleranz um den erwarteten Tick + Offset = 0.0, -- Sek. Vorlauf (Latenzausgleich): Funke laeuft frueher los + SparkW = 2, -- px Funkenbreite +}; + +-- ========================================================================== +-- [ Texte ] +-- ========================================================================== + +FBLocale["enUS"].TAB_TICKER = "Extras"; +FBLocale["enUS"].TICK_HEADER = "Mana ticker"; +FBLocale["enUS"].TICK_ENABLED = "Mana ticker"; +FBLocale["enUS"].TICK_ENABLED_TIP = "A spark travels across your own mana bar every 2 seconds, in step with the server's mana regeneration tick. Cast right after the spark reaches the end and you lose no regeneration. Needs the mana bar to be on."; +FBLocale["enUS"].TICK_FSR = "Five-second rule"; +FBLocale["enUS"].TICK_FSR_TIP = "After you spend mana the spark turns orange and runs over the five seconds (plus the wait for the next tick) until spirit regeneration resumes."; +FBLocale["enUS"].TICK_TOLERANCE = "Tick tolerance: |cFFFFFFFF%s s"; +FBLocale["enUS"].TICK_OFFSET = "Tick offset: |cFFFFFFFF%s s"; +FBLocale["enUS"].TICK_SPARKW = "Spark width: |cFFFFFFFF%s px"; +FBLocale["enUS"].TICK_INFO = "White spark: time until the next regeneration tick. Orange spark: five-second rule running, regeneration resumes when it reaches the end. Raise the tolerance if ticks are missed (totems, Innervate); use the offset if the spark arrives late compared to your mana jumps."; +FBLocale["enUS"].TICK_STATUS = "Ticker: %s, next tick in %.1f s%s"; +FBLocale["enUS"].TICK_SYNCED = "synced"; +FBLocale["enUS"].TICK_UNSYNCED = "waiting for first tick"; +FBLocale["enUS"].TICK_STATUS_FSR = ", five-second rule %.1f s"; +FBLocale["enUS"].TICK_LOADED = "Mana ticker module loaded: tab |cFFFFFFFFTicker|r in the options, /fbp ticker"; +FBLocale["enUS"].TICK_ON = "Mana ticker |cFF00FF00on|r."; +FBLocale["enUS"].TICK_OFF = "Mana ticker |cFFFF0000off|r."; + +FBLocale["deDE"].TAB_TICKER = "Extras"; +FBLocale["deDE"].TICK_HEADER = "Mana-Ticker"; +FBLocale["deDE"].TICK_ENABLED = "Mana-Ticker"; +FBLocale["deDE"].TICK_ENABLED_TIP = "Ein Funke wandert alle 2 Sekunden ueber deinen eigenen Manastreifen, im Takt des Regenerationsticks des Servers. Wer direkt nach dem Funken am Ende castet, verliert keine Regeneration. Braucht den eingeschalteten Manabalken."; +FBLocale["deDE"].TICK_FSR = "Fuenf-Sekunden-Regel"; +FBLocale["deDE"].TICK_FSR_TIP = "Nach jedem Manaverbrauch wird der Funke orange und laeuft ueber die fuenf Sekunden (plus Wartezeit bis zum naechsten Tick), bis die Willenskraft-Regeneration wieder einsetzt."; +FBLocale["deDE"].TICK_TOLERANCE = "Tick-Toleranz: |cFFFFFFFF%s s"; +FBLocale["deDE"].TICK_OFFSET = "Tick-Vorlauf: |cFFFFFFFF%s s"; + +-- Weitere Sprachen (Spanisch, Franzoesisch, Italienisch) + +FBLocale["esES"].TAB_TICKER = "Extras"; +FBLocale["esES"].TICK_HEADER = "Ticker de maná"; +FBLocale["esES"].TICK_ENABLED = "Ticker de maná"; +FBLocale["esES"].TICK_ENABLED_TIP = "Una chispa recorre tu propia barra de maná cada 2 segundos, al compás del pulso de regeneración de maná del servidor. Lanza justo después de que la chispa llegue al final y no perderás regeneración. Requiere la barra de maná activada."; +FBLocale["esES"].TICK_FSR = "Regla de los cinco segundos"; +FBLocale["esES"].TICK_FSR_TIP = "Tras gastar maná, la chispa se vuelve naranja y recorre los cinco segundos (más la espera hasta el siguiente pulso) hasta que la regeneración por espíritu se reanuda."; +FBLocale["esES"].TICK_TOLERANCE = "Tolerancia del pulso: |cFFFFFFFF%s s"; +FBLocale["esES"].TICK_OFFSET = "Desfase del pulso: |cFFFFFFFF%s s"; +FBLocale["esES"].TICK_SPARKW = "Ancho de la chispa: |cFFFFFFFF%s px"; +FBLocale["esES"].TICK_INFO = "Chispa blanca: tiempo hasta el siguiente pulso de regeneración. Chispa naranja: regla de los cinco segundos en curso, la regeneración se reanuda cuando llega al final. Sube la tolerancia si se pierden pulsos (tótems, Estimular); usa el desfase si la chispa llega tarde respecto a tus saltos de maná."; +FBLocale["esES"].TICK_STATUS = "Ticker: %s, siguiente pulso en %.1f s%s"; +FBLocale["esES"].TICK_SYNCED = "sincronizado"; +FBLocale["esES"].TICK_UNSYNCED = "esperando el primer pulso"; +FBLocale["esES"].TICK_STATUS_FSR = ", regla de los cinco segundos %.1f s"; +FBLocale["esES"].TICK_LOADED = "Módulo de ticker de maná cargado: pestaña |cFFFFFFFFExtras|r en las opciones, /fbp ticker"; +FBLocale["esES"].TICK_ON = "Ticker de maná |cFF00FF00activado|r."; +FBLocale["esES"].TICK_OFF = "Ticker de maná |cFFFF0000desactivado|r."; + +FBLocale["frFR"].TAB_TICKER = "Extras"; +FBLocale["frFR"].TICK_HEADER = "Ticker de mana"; +FBLocale["frFR"].TICK_ENABLED = "Ticker de mana"; +FBLocale["frFR"].TICK_ENABLED_TIP = "Une étincelle parcourt votre propre barre de mana toutes les 2 secondes, au rythme du tick de régénération de mana du serveur. Lancez juste après que l'étincelle atteint la fin et vous ne perdez aucune régénération. Nécessite la barre de mana activée."; +FBLocale["frFR"].TICK_FSR = "Règle des cinq secondes"; +FBLocale["frFR"].TICK_FSR_TIP = "Après une dépense de mana, l'étincelle devient orange et parcourt les cinq secondes (plus l'attente du prochain tick) jusqu'à la reprise de la régénération par l'esprit."; +FBLocale["frFR"].TICK_TOLERANCE = "Tolérance du tick : |cFFFFFFFF%s s"; +FBLocale["frFR"].TICK_OFFSET = "Décalage du tick : |cFFFFFFFF%s s"; +FBLocale["frFR"].TICK_SPARKW = "Largeur de l'étincelle : |cFFFFFFFF%s px"; +FBLocale["frFR"].TICK_INFO = "Étincelle blanche : temps jusqu'au prochain tick de régénération. Étincelle orange : règle des cinq secondes en cours, la régénération reprend quand elle atteint la fin. Augmentez la tolérance si des ticks sont manqués (totems, Innervation) ; utilisez le décalage si l'étincelle arrive en retard par rapport à vos sauts de mana."; +FBLocale["frFR"].TICK_STATUS = "Ticker : %s, prochain tick dans %.1f s%s"; +FBLocale["frFR"].TICK_SYNCED = "synchronisé"; +FBLocale["frFR"].TICK_UNSYNCED = "en attente du premier tick"; +FBLocale["frFR"].TICK_STATUS_FSR = ", règle des cinq secondes %.1f s"; +FBLocale["frFR"].TICK_LOADED = "Module ticker de mana chargé : onglet |cFFFFFFFFExtras|r dans les options, /fbp ticker"; +FBLocale["frFR"].TICK_ON = "Ticker de mana |cFF00FF00activé|r."; +FBLocale["frFR"].TICK_OFF = "Ticker de mana |cFFFF0000désactivé|r."; + +FBLocale["itIT"].TAB_TICKER = "Extra"; +FBLocale["itIT"].TICK_HEADER = "Ticker del mana"; +FBLocale["itIT"].TICK_ENABLED = "Ticker del mana"; +FBLocale["itIT"].TICK_ENABLED_TIP = "Una scintilla attraversa la tua barra del mana ogni 2 secondi, a tempo con il tick di rigenerazione del mana del server. Lancia subito dopo che la scintilla arriva in fondo e non perdi rigenerazione. Richiede la barra del mana attiva."; +FBLocale["itIT"].TICK_FSR = "Regola dei cinque secondi"; +FBLocale["itIT"].TICK_FSR_TIP = "Dopo aver speso mana la scintilla diventa arancione e percorre i cinque secondi (più l'attesa del tick successivo) finché la rigenerazione da spirito riprende."; +FBLocale["itIT"].TICK_TOLERANCE = "Tolleranza del tick: |cFFFFFFFF%s s"; +FBLocale["itIT"].TICK_OFFSET = "Anticipo del tick: |cFFFFFFFF%s s"; +FBLocale["itIT"].TICK_SPARKW = "Larghezza scintilla: |cFFFFFFFF%s px"; +FBLocale["itIT"].TICK_INFO = "Scintilla bianca: tempo al prossimo tick di rigenerazione. Scintilla arancione: regola dei cinque secondi in corso, la rigenerazione riprende quando arriva in fondo. Alza la tolleranza se i tick vengono persi (totem, Innervazione); usa l'anticipo se la scintilla arriva in ritardo rispetto ai tuoi salti di mana."; +FBLocale["itIT"].TICK_STATUS = "Ticker: %s, prossimo tick tra %.1f s%s"; +FBLocale["itIT"].TICK_SYNCED = "sincronizzato"; +FBLocale["itIT"].TICK_UNSYNCED = "in attesa del primo tick"; +FBLocale["itIT"].TICK_STATUS_FSR = ", regola dei cinque secondi %.1f s"; +FBLocale["itIT"].TICK_LOADED = "Modulo ticker del mana caricato: scheda |cFFFFFFFFExtra|r nelle opzioni, /fbp ticker"; +FBLocale["itIT"].TICK_ON = "Ticker del mana |cFF00FF00attivo|r."; +FBLocale["itIT"].TICK_OFF = "Ticker del mana |cFFFF0000disattivato|r."; +FBLocale["deDE"].TICK_SPARKW = "Funkenbreite: |cFFFFFFFF%s px"; +FBLocale["deDE"].TICK_INFO = "Weisser Funke: Zeit bis zum naechsten Regenerationstick. Oranger Funke: Fuenf-Sekunden-Regel laeuft, am Ende setzt die Regeneration wieder ein. Toleranz erhoehen, wenn Ticks verpasst werden (Totems, Anregen); Vorlauf nutzen, wenn der Funke spaeter ankommt als deine Manaspruenge."; +FBLocale["deDE"].TICK_STATUS = "Ticker: %s, naechster Tick in %.1f s%s"; +FBLocale["deDE"].TICK_SYNCED = "synchron"; +FBLocale["deDE"].TICK_UNSYNCED = "wartet auf ersten Tick"; +FBLocale["deDE"].TICK_STATUS_FSR = ", Fuenf-Sekunden-Regel %.1f s"; +FBLocale["deDE"].TICK_LOADED = "Mana-Ticker-Modul geladen: Reiter |cFFFFFFFFTicker|r in den Optionen, /fbp ticker"; +FBLocale["deDE"].TICK_ON = "Mana-Ticker |cFF00FF00an|r."; +FBLocale["deDE"].TICK_OFF = "Mana-Ticker |cFFFF0000aus|r."; + +-- ========================================================================== +-- [ Zustand ] +-- ========================================================================== + +FBTicker = { + lastTick = nil, -- GetTime() des letzten akzeptierten Ticks (Rasterpunkt) + lastMana = nil, -- zuletzt gesehener Manawert + tickSize = nil, -- gelernte typische Tickgroesse (fuer den Trankfilter) + misses = 0, -- Kandidaten in Folge ausserhalb der Toleranz + fsrStart = nil, -- Beginn der Fuenf-Sekunden-Regel + fsrUntil = nil, -- ihr Ende (5 s nach Beginn) + sparks = {}, -- Funken auf den Manabalken (Plakette, Raid-Zelle) +}; + +function FBTicker_Cfg() + if (not HealBox.Ticker) then FBTicker_ApplyDefaults(); end + return HealBox.Ticker; +end + +function FBTicker_ApplyDefaults() + if (not HealBox.Ticker) then HealBox.Ticker = {}; end + for k, v in pairs(FBTickerDefaults) do + if (HealBox.Ticker[k] == nil) then HealBox.Ticker[k] = v; end + end + return true; +end + +-- Volles Mana? (dann gibt es nichts zu regenerieren, Funke aus) +function FBTicker_ManaFull() + local mp, mpMax = UnitMana("player"), UnitManaMax("player"); + if (not mp) or (not mpMax) or (mpMax <= 0) then return true; end + return (mp >= mpMax); +end + +-- Nutzt der Spieler gerade Mana? (Druiden in Gestalt: nein) +function FBTicker_PlayerHasMana() + if (UnitPowerType and UnitPowerType("player") ~= 0) then return false; end + local max = UnitManaMax("player"); + return (max and max > 0); +end + +-- ========================================================================== +-- [ Erkennung ] +-- ========================================================================== + +-- Naechster Rasterpunkt ab Zeitpunkt t (t selbst, wenn t genau auf dem Raster liegt) +function FBTicker_NextTickAfter(t) + local last = FBTicker.lastTick; + if (not last) then return nil; end + if (t <= last) then return last; end + local k = math.ceil((t - last) / FBTICK_PERIOD - 0.0001); + return last + k * FBTICK_PERIOD; +end + +-- Abstand von t zum naechstgelegenen Rasterpunkt +function FBTicker_GridDistance(t) + local last = FBTicker.lastTick; + if (not last) then return nil; end + local phase = math.mod(t - last, FBTICK_PERIOD); + if (phase < 0) then phase = phase + FBTICK_PERIOD; end + if (phase > FBTICK_PERIOD / 2) then phase = FBTICK_PERIOD - phase; end + return phase; +end + +function FBTicker_OnManaChanged() + local cfg = FBTicker_Cfg(); + local now = GetTime(); + local mana = UnitMana("player") or 0; + local prev = FBTicker.lastMana; + FBTicker.lastMana = mana; + if (prev == nil) then return; end + local delta = mana - prev; + + if (delta < 0) then + -- Mana ausgegeben: Fuenf-Sekunden-Regel beginnt jetzt + FBTicker.fsrStart = now; + FBTicker.fsrUntil = now + FBTICK_FSR; + return; + end + if (delta == 0) then return; end + + -- Zuwachs: Trank oder Rune? (grosser Sprung im Vergleich zur Tickgroesse) + if (FBTicker.tickSize and delta >= FBTICK_POTION_MIN and delta > FBTicker.tickSize * FBTICK_POTION_FACT) then + return; + end + + if (not FBTicker.lastTick) then + FBTicker.lastTick = now; + FBTicker.misses = 0; + else + local dist = FBTicker_GridDistance(now); + if (dist <= (cfg.Tolerance or 0.3)) then + FBTicker.lastTick = now; -- passt ins Raster: neu synchronisieren + FBTicker.misses = 0; + else + FBTicker.misses = FBTicker.misses + 1; + if (FBTicker.misses >= FBTICK_RESYNC_MISS) then + FBTicker.lastTick = now; -- Raster hat sich verschoben + FBTicker.misses = 0; + end + return; -- fremder Puls, Tickgroesse nicht lernen + end + end + + -- Tickgroesse lernen (gleitend), nur ausserhalb der Regel (volle Ticks) + local inFSR = (FBTicker.fsrUntil and now < FBTicker.fsrUntil); + if (not inFSR) then + if (not FBTicker.tickSize) then + FBTicker.tickSize = delta; + else + FBTicker.tickSize = FBTicker.tickSize * 0.7 + delta * 0.3; + end + end +end + +-- Zeitpunkt, an dem die Regeneration nach der Regel wieder einsetzt: +-- erster Rasterpunkt nach fsrUntil (ohne Raster: fsrUntil selbst) +function FBTicker_FSREnd() + if (not FBTicker.fsrUntil) then return nil; end + local grid = FBTicker_NextTickAfter(FBTicker.fsrUntil); + if (grid) then return grid; end + return FBTicker.fsrUntil; +end + +-- Anteil 0..1 und Modus ("tick" | "fsr" | nil) fuer den Funken zur Zeit now +function FBTicker_Progress(now) + local cfg = FBTicker_Cfg(); + local t = now + (cfg.Offset or 0); + + if (cfg.FSR == 1 and FBTicker.fsrStart) then + local fsrEnd = FBTicker_FSREnd(); + if (fsrEnd and t < fsrEnd) then + local total = fsrEnd - FBTicker.fsrStart; + if (total <= 0) then total = FBTICK_FSR; end + local frac = (t - FBTicker.fsrStart) / total; + if (frac < 0) then frac = 0; end + if (frac > 1) then frac = 1; end + return frac, "fsr"; + end + -- Regel vorbei + FBTicker.fsrStart = nil; + FBTicker.fsrUntil = nil; + end + + if (not FBTicker.lastTick) then return nil, nil; end + local phase = math.mod(t - FBTicker.lastTick, FBTICK_PERIOD); + if (phase < 0) then phase = phase + FBTICK_PERIOD; end + return phase / FBTICK_PERIOD, "tick"; +end + +function FBTicker_Reset() + FBTicker.lastTick = nil; + FBTicker.lastMana = UnitMana("player"); + FBTicker.tickSize = nil; + FBTicker.misses = 0; + FBTicker.fsrStart = nil; + FBTicker.fsrUntil = nil; +end + +-- ========================================================================== +-- [ Funken ] +-- ========================================================================== + +-- Funke auf einem Manabalken anlegen (einmal je Balken) +function FBTicker_SparkFor(bar) + if (not bar) then return nil; end + if (bar.fbSpark) then return bar.fbSpark; end + local s = bar:CreateTexture(nil, "OVERLAY"); + s:SetTexture(1, 1, 1, 1); + s:SetWidth(2); + s:SetHeight(4); + s:SetPoint("LEFT", bar, "LEFT", 0, 0); + s:Hide(); + bar.fbSpark = s; + return s; +end + +-- Balken einsammeln: eigene Plakette und, falls vorhanden, eigene Raid-Zelle +function FBTicker_CollectBars() + FBTicker.sparks = {}; + if (FBHealBox1 and FBHealBox1.ManaBar) then + table.insert(FBTicker.sparks, { bar = FBHealBox1.ManaBar, spark = FBTicker_SparkFor(FBHealBox1.ManaBar) }); + end + if (FBRaidUnitCell and FBRaidTestMode == 0) then + local me = UnitName("player"); + for unit, cell in pairs(FBRaidUnitCell) do + if (cell.name == me and cell.ManaBar) then + table.insert(FBTicker.sparks, { bar = cell.ManaBar, spark = FBTicker_SparkFor(cell.ManaBar) }); + end + end + end +end + +function FBTicker_HideAll() + for _, e in ipairs(FBTicker.sparks) do + if (e.spark) then e.spark:Hide(); end + end +end + +function FBTicker_Draw() + local cfg = FBTicker_Cfg(); + if (cfg.Enabled ~= 1) or (HealBox.Active ~= 1) or (not FBTicker_PlayerHasMana()) then + FBTicker_HideAll(); + return; + end + -- Volles Mana: nichts zu regenerieren, Funke aus + if (FBTicker_ManaFull()) then + FBTicker_HideAll(); + return; + end + local frac, mode = FBTicker_Progress(GetTime()); + if (not frac) then + FBTicker_HideAll(); + return; + end + local col = FBTICK_COLOR_TICK; + if (mode == "fsr") then col = FBTICK_COLOR_FSR; end + local w = cfg.SparkW or 2; + + for _, e in ipairs(FBTicker.sparks) do + local bar, s = e.bar, e.spark; + if (s and bar) then + if (bar:IsShown()) then + local bw = bar:GetWidth() or 0; + local bh = bar:GetHeight() or 0; + if (bw > w and bh > 0) then + s:SetWidth(w); + s:SetHeight(bh); + if (e.mode ~= mode) then + e.mode = mode; + s:SetTexture(col[1], col[2], col[3], col[4]); + end + s:ClearAllPoints(); + s:SetPoint("LEFT", bar, "LEFT", frac * (bw - w), 0); + s:Show(); + else + s:Hide(); + end + else + s:Hide(); + end + end + end +end + +-- ========================================================================== +-- [ Events ] +-- ========================================================================== + +FBTickerFrame = CreateFrame("Frame", "FBHealBoxTickerFrame", UIParent); +FBTickerFrame:RegisterEvent("UNIT_MANA"); +FBTickerFrame:RegisterEvent("UNIT_MAXMANA"); +FBTickerFrame:RegisterEvent("UNIT_DISPLAYPOWER"); +FBTickerFrame:RegisterEvent("PLAYER_ENTERING_WORLD"); +FBTickerFrame:SetScript("OnEvent", function() + if (event == "PLAYER_ENTERING_WORLD") then + FBTicker_Reset(); + FBTicker_CollectBars(); + return; + end + if (arg1 ~= "player") then return; end + if (event == "UNIT_MANA") then + FBTicker_OnManaChanged(); + if (FBTicker_ManaFull()) then FBTicker_HideAll(); end + elseif (event == "UNIT_DISPLAYPOWER") then + FBTicker_Reset(); + else + FBTicker.lastMana = UnitMana("player"); + end +end); +FBTickerFrame:SetScript("OnUpdate", function() + FBTicker_Draw(); +end); + +-- ========================================================================== +-- [ Options-Reiter "Ticker" ] +-- ========================================================================== + +FBTickerSliders = {}; + +function FBTicker_SliderText(s) + if (not s or not s.Text) then return; end + local v = s:GetValue(); + local shown; + if (s.decimals) then shown = format("%.1f", v); else shown = tostring(math.floor(v + 0.5)); end + s.Text:SetText(format(FBT(s.labelKey), shown)); +end + +function FBTicker_CreateSlider(name, parent, x, y, labelKey, cfgKey, minV, maxV, step, decimals) + local s = CreateFrame("Slider", name, parent, "OptionsSliderTemplate"); + s:SetWidth(128); + s:SetHeight(16); + s:SetPoint("TOPLEFT", x, y); + s:SetMinMaxValues(minV, maxV); + s:SetValueStep(step); + s.labelKey = labelKey; + s.cfgKey = cfgKey; + s.decimals = decimals; + s.Text = s:CreateFontString(nil, "BACKGROUND", "GameFontNormal"); + s.Text:SetPoint("CENTER", 0, 15); + getglobal(name.."Low"):SetText(tostring(minV)); + getglobal(name.."High"):SetText(tostring(maxV)); + s:SetValue(FBTicker_Cfg()[cfgKey] or minV); + FBTicker_SliderText(s); + s:SetScript("OnValueChanged", function() + local v = s:GetValue(); + if (s.decimals) then v = math.floor(v * 10 + 0.5) / 10; else v = math.floor(v + 0.5); end + FBTicker_Cfg()[s.cfgKey] = v; + FBTicker_SliderText(s); + end); + FBTickerSliders[cfgKey] = s; + return s; +end + +function FBTicker_BuildOptions() + local tab = FBHealBox_AddOptionsTab("TAB_TICKER"); + if (not tab) then return; end + -- Abschnittskopf: der Reiter "Extras" wird mit Smart Damage geteilt + FBTickerHeader = tab:CreateFontString(nil, "OVERLAY", "GameFontNormal"); + FBTickerHeader:SetPoint("TOPLEFT", tab, "TOPLEFT", 35, FBOPT_CONTENT_Y); + FBTickerHeader:SetText(FBT("TICK_HEADER")); + local y = FBOPT_CONTENT_Y - 26; + + FBTickerEnabledCheck = FBHealBox_CreateCheck("FBHealBoxTickerEnabledCheck", tab, 40, y, "TICK_ENABLED", "TICK_ENABLED_TIP", function() + FBTicker_Cfg().Enabled = FBTickerEnabledCheck:GetChecked() and 1 or 0; + FBTicker_Draw(); + end); + FBTickerFSRCheck = FBHealBox_CreateCheck("FBHealBoxTickerFSRCheck", tab, 250, y, "TICK_FSR", "TICK_FSR_TIP", function() + FBTicker_Cfg().FSR = FBTickerFSRCheck:GetChecked() and 1 or 0; + FBTicker_Draw(); + end); + + local sy = y - 60; + FBTicker_CreateSlider("FBTickerToleranceSlider", tab, 75, sy, "TICK_TOLERANCE", "Tolerance", 0.1, 0.6, 0.1, true); + FBTicker_CreateSlider("FBTickerOffsetSlider", tab, 260, sy, "TICK_OFFSET", "Offset", 0.0, 0.5, 0.1, true); + FBTicker_CreateSlider("FBTickerSparkWSlider", tab, 75, sy - 46, "TICK_SPARKW", "SparkW", 1, 4, 1, false); + + FBTickerInfoText = tab:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall"); + FBTickerInfoText:SetPoint("TOPLEFT", tab, "TOPLEFT", 35, sy - 84); + FBTickerInfoText:SetWidth(395); + FBTickerInfoText:SetJustifyH("LEFT"); + FBTickerInfoText:SetText(FBT("TICK_INFO")); + + FBTicker_SyncOptions(); +end + +function FBTicker_SyncOptions() + local cfg = FBTicker_Cfg(); + if (FBTickerEnabledCheck) then FBTickerEnabledCheck:SetChecked(cfg.Enabled == 1); end + if (FBTickerFSRCheck) then FBTickerFSRCheck:SetChecked(cfg.FSR == 1); end + for key, s in pairs(FBTickerSliders) do + if (cfg[key] ~= nil) then s:SetValue(cfg[key]); end + end + return true; +end + +function FBTicker_ApplyLocale() + if (FBTickerHeader) then FBTickerHeader:SetText(FBT("TICK_HEADER")); end + if (FBTickerEnabledCheck) then FBTickerEnabledCheck.Text:SetText(FBT("TICK_ENABLED")); FBTickerEnabledCheck.tooltipText = FBT("TICK_ENABLED_TIP"); end + if (FBTickerFSRCheck) then FBTickerFSRCheck.Text:SetText(FBT("TICK_FSR")); FBTickerFSRCheck.tooltipText = FBT("TICK_FSR_TIP"); end + if (FBTickerInfoText) then FBTickerInfoText:SetText(FBT("TICK_INFO")); end + for _, s in pairs(FBTickerSliders) do FBTicker_SliderText(s); end + return true; +end + +-- ========================================================================== +-- [ Anbindung an den Kern ] +-- ========================================================================== + +FBHealBox_RegisterHook("Loaded", function() + DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..FBT("TICK_LOADED")); + return true; +end); +FBHealBox_RegisterHook("Defaults", function() return FBTicker_ApplyDefaults(); end); +FBHealBox_RegisterHook("SyncOptions", function() return FBTicker_SyncOptions(); end); +FBHealBox_RegisterHook("ApplyLocale", function() return FBTicker_ApplyLocale(); end); +FBHealBox_RegisterHook("UpdateNames", function() FBTicker_CollectBars(); return true; end); +FBHealBox_RegisterHook("ActiveToggle", function() FBTicker_Draw(); return true; end); +FBHealBox_RegisterHook("Status", function() + local state = FBT("TICK_UNSYNCED"); + local nextIn = 0; + if (FBTicker.lastTick) then + state = FBT("TICK_SYNCED"); + local nxt = FBTicker_NextTickAfter(GetTime()); + if (nxt) then nextIn = nxt - GetTime(); end + end + local fsr = ""; + local fsrEnd = FBTicker_FSREnd(); + if (fsrEnd and fsrEnd > GetTime()) then fsr = format(FBT("TICK_STATUS_FSR"), fsrEnd - GetTime()); end + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FFFF[FBP]|r "..format(FBT("TICK_STATUS"), state, nextIn, fsr)); + return true; +end); +FBHealBox_RegisterHook("Slash", function(msg) + if (msg == "ticker") then + local cfg = FBTicker_Cfg(); + if (cfg.Enabled == 1) then cfg.Enabled = 0; else cfg.Enabled = 1; end + FBTicker_SyncOptions(); + FBTicker_Draw(); + local key = "TICK_OFF"; + if (cfg.Enabled == 1) then key = "TICK_ON"; end + DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00"..FBADDON_NAME..":|r "..FBT(key)); + return true; + end + return false; +end); + +FBTicker_ApplyDefaults(); +FBTicker_BuildOptions(); +FBTicker_CollectBars();