-- UI/Glow.lua — Bildschirmrand-Puls bei Warnung. Vollbild-Frame (FULLSCREEN-Strata, keine Maus). -- DESIGN-V2 5.4: W.TEXTUR_ROT war "Interface\FullScreenTextures\LowHealth" - exakt die Textur, die der -- Client selbst ueber LowHealthFrame einblendet, sobald das Leben unter 20 % faellt. Bei HP20 lagen -- also zwei identische rote Vignetten uebereinander; der Spieler konnte "das Spiel warnt" und -- "Lyra warnt" nicht unterscheiden. Lyras Alarm ist seitdem VIOLETT und selbst gezeichnet. -- REVIEW7 / DESIGN-V3 B-6 (17.09.2026): Ohne Gradient-API vier einfarbige Baender mit harter Kante. -- -- W7 (20.09.2026, Screenshot-Befund Harald "die flash zones koennten cooler sein"): -- BEFUND Auf Bildschirmfoto_20260920_112620.png liegt kein Randgluehen, sondern ein harter, -- weisslich ausgewaschener Block ueber je 18 % jeder Kante - mit scharfer Innenkante bei -- exakt x = 346 (1920 * 0.18). Gemessen: von x = 20 bis x = 345 durchgehend gesaettigt, -- dann ein Sprung auf das Spielbild. Es gibt KEINEN Verlauf, und die Farbe ist WEISS, -- nicht Lyras Violett. -- URSACHE Nicht die fehlende Gradient-API. Der alte faerbe() rief SetGradient (das lief durch, -- deshalb die 18 % und nicht die 12 % des harten Rueckfalls) und danach -- t:SetVertexColor(1, 1, 1, 1) - und eine Vertexfarbe NACH einem Verlauf setzt die -- Textur auf genau diese eine Farbe zurueck. Der Verlauf war weg, Weiss blieb, ADD-Blend -- ueber hellem Wuestensand ergibt die ausgewaschene Flaeche. Zwei APIs, die sich -- gegenseitig ueberschreiben, in derselben Funktion, in dieser Reihenfolge. -- FIX Kein Gradient mehr. Eine EIGENE Textur bilder/puls_rand.png (256 x 256, Alpha-Verlauf -- von aussen nach innen, Ecken ueber eine Screen-Verknuepfung weich zusammengefuehrt) -- liegt als EIN Bild ueber dem ganzen Bildschirm. Begruendung gegen SetGradient: -- * vier Rechtecke mit Verlauf haben keine Ecken - sie ueberlappen sich dort und -- addieren sich im ADD-Blend zum doppelten Wert. Genau die vier hellen Ecken, die -- auf dem Screenshot auffallen. -- * SetGradient und SetVertexColor teilen sich denselben Farbzustand. Die Faerbung -- nach Warnstufe braucht aber BEIDES - mit einer Alpha-Textur genuegt SetVertexColor, -- und der Verlauf steckt unveraenderlich im Bild. -- * eine Textur laedt auf jedem der fuenf Clients gleich; Gradient-Verhalten nicht. -- Faellt die PNG aus, bleibt der alte Weg als Rueckfall (vier Baender, jetzt schmaler -- und ohne den toten Gradient-Zweig). W.weich sagt, welcher Pfad laeuft. -- STUFEN 1 Hinweis: Violett, sehr dezent. 2 Warnung: Violett, kraeftiger. 3 Alarm: Rot-Violett -- mit kurzem DOPPELSCHLAG (Herzschlag) - Form statt nur Farbe, dieselbe Regel wie beim -- Formsymbol der Blase (Welle 6). Standzeit <= 1 s, Ausklang weich. -- Barrierefreiheit: Recherche 02 2.3 (Photosensibilitaet) ist die Obergrenze. Der Riegel zwischen -- zwei Pulsen bleibt; die Flaeche ist mit dem Verlauf deutlich kleiner als der alte Block, darum -- darf der Barrierefrei-Modus die Spitze ANHEBEN (W.BF_FAKTOR) statt sie zu senken - wer den -- Bildschirmrand als Warnkanal braucht, braucht ihn sichtbar. -- Streamer-Modus: kein Puls. Ein Vollbild-Blitz im Stream ist fuer die Zuschauer, nicht fuer den -- Spieler - und der Chroma-Key frisst ihn ohnehin. -- Nie im Ladebildschirm-Riegel, nie wenn tot. Einstellung "glow" (Default true). -- API: CreateFrame, Texture:SetTexture/SetColorTexture/SetVertexColor/SetBlendMode (guarded), -- AnimationGroup (guarded, mit C_Timer-Rueckfall), UnitIsDeadOrGhost. Nichts Fremdes. local ADDON, ns = ... local W = {} ns.Glow = W W.DAUER = 0.9 W.SPITZE = 0.45 W.RIEGEL = 4 -- s zwischen zwei Pulsen W.BAND = 0.18 -- W7: nur noch fuer den Rueckfall (vier Baender) W.FARBEN = { lila = { 0.706, 0.549, 1.0 }, -- #b48cff - Lyras Farbe, eindeutig NICHT Blizzards Rot blau = { 0.451, 0.702, 1.0 }, -- #73B3FF - Atem/Wasser -- W7: Stufe 3. Rot-Violett, nicht Blizzards Rot (#ff0000) und nicht Lyras Lila - dazwischen. rotlila = { 1.0, 0.357, 0.706 }, -- #ff5ab4 } -- W7: Die drei Warnstufen. dauer <= 1 s (Zusage aus dem Befund), schlaege = Doppelschlag. W.STUFEN = { [1] = { farbe = "lila", spitze = 0.22, dauer = 0.70, schlaege = 1 }, [2] = { farbe = "lila", spitze = 0.34, dauer = 0.85, schlaege = 1 }, [3] = { farbe = "rotlila", spitze = 0.48, dauer = 1.00, schlaege = 2 }, } W.TEXTUR = ns.PFAD .. "bilder\\puls_rand.png" W.WEISS = "Interface\\Buttons\\WHITE8X8" W.BAND_HART = 0.12 W.SPITZE_HART = 0.35 W.SPITZE_MAX = 0.58 -- W7: harte Obergrenze, egal was jemand hineinreicht (Photosensibilitaet) W.weich = false -- true = die weiche Vignette laeuft; false = Rueckfall vier Baender W.hartesBand = false -- /lyra debug zeigt, welcher Pfad laeuft (Rueckfall aktiv) W.zuletzt = 0 -- REVIEW: kein Frame-Name -> kein zusaetzlicher Global local f = CreateFrame("Frame", nil, UIParent) f:SetFrameStrata("FULLSCREEN") f:SetAllPoints(UIParent) f:EnableMouse(false) f:SetAlpha(0) f:Hide() W.frame = f -- --------------------------------------------------------------------------------------------- -- W7: die weiche Vignette. EINE Textur ueber den ganzen Bildschirm, der Verlauf steckt im Bild. -- --------------------------------------------------------------------------------------------- local vignette = f:CreateTexture(nil, "BACKGROUND") vignette:SetAllPoints(f) if vignette.SetBlendMode then pcall(vignette.SetBlendMode, vignette, "ADD") end do local ok = pcall(vignette.SetTexture, vignette, W.TEXTUR) local wert if ok and vignette.GetTexture then local o, v = pcall(vignette.GetTexture, vignette) wert = o and v or nil end -- Auf einem Client ohne diese Datei gibt GetTexture nil oder "" zurueck. Dann NICHT so tun, -- als laege da ein Verlauf - sonst stuende wieder eine deckende weisse Flaeche im Bild. -- type(wert) == "string" und nicht nur "~= nil": eine Textur, deren Pfad nicht aufgeht, gibt -- auf manchen Wegen auch einen anderen Typ zurueck, und "irgendetwas ist da" ist hier keine -- Zusage, die reicht - es haengt eine bildschirmfuellende Flaeche daran. W.weich = (ok and type(wert) == "string" and wert ~= "") and true or false end if not W.weich then vignette:Hide() end W.vignette = vignette -- --------------------------------------------------------------------------------------------- -- Rueckfall: vier Rand-Texturen wie bis 0.10.0, nur schmaler und ohne den toten Gradient-Zweig. -- Sie werden NUR angelegt und gezeigt, wenn die Vignette fehlt. -- --------------------------------------------------------------------------------------------- local raender = {} local function neueKante(punkt1, punkt2) local t = f:CreateTexture(nil, "BACKGROUND") local ok = pcall(t.SetTexture, t, W.WEISS) if not ok and t.SetColorTexture then pcall(t.SetColorTexture, t, 1, 1, 1, 1) end if t.SetBlendMode then pcall(t.SetBlendMode, t, "ADD") end t:SetPoint(punkt1, f, punkt1, 0, 0) t:SetPoint(punkt2, f, punkt2, 0, 0) t:Hide() raender[#raender + 1] = t return t end local oben = neueKante("TOPLEFT", "TOPRIGHT") local unten = neueKante("BOTTOMLEFT", "BOTTOMRIGHT") local links = neueKante("TOPLEFT", "BOTTOMLEFT") local rechts = neueKante("TOPRIGHT", "BOTTOMRIGHT") W.raender = raender -- Farbe setzen. EINE API pro Textur (SetVertexColor) - genau der Fehler aus dem Befund war, -- zwei sich ueberschreibende Farbwege in einer Funktion zu haben. local function faerbe(farbe) local c = W.FARBEN[farbe] or W.FARBEN.lila local r, g, b = c[1], c[2], c[3] if W.weich then pcall(vignette.SetVertexColor, vignette, r, g, b, 1) W.hartesBand = false else for _, t in ipairs(raender) do pcall(t.SetVertexColor, t, r, g, b, 1) end W.hartesBand = true end W.farbeJetzt = farbe return not W.hartesBand end W.faerbe = faerbe local function bemesse(spitze) if W.weich then vignette:SetAlpha(spitze) vignette:Show() for _, t in ipairs(raender) do t:Hide() end return end local w = (UIParent.GetWidth and UIParent:GetWidth()) or 1920 local h = (UIParent.GetHeight and UIParent:GetHeight()) or 1080 local band = W.BAND_HART oben:SetHeight(h * band); unten:SetHeight(h * band) links:SetWidth(w * band); rechts:SetWidth(w * band) for _, t in ipairs(raender) do t:SetAlpha(spitze); t:Show() end end -- --------------------------------------------------------------------------------------------- -- Puls-Kurve. Frame-Alpha 0 -> 1 -> 0; die Spitze steuert die Textur-Alpha (effektiv = Produkt). -- Stufe 3 bekommt einen Doppelschlag: auf, halb zurueck, auf, weich aus. Das ist eine FORM und -- damit ein zweiter Traeger neben der Farbe (Recherche 10, A5). -- Keine AnimationGroup an der Gestalt (Bewegung v3) - dies ist der eigene Puls-Frame, und die -- Gruppe ist guarded gebaut UND hat einen Rueckfall ohne Animation. -- --------------------------------------------------------------------------------------------- W.DOPPEL_TAL = 0.35 -- auf welchen Anteil der Puls zwischen den zwei Schlaegen zurueckgeht local ag, stufen -- stufen = { anim1, anim2, anim3, anim4 } do if f.CreateAnimationGroup then local ok, g = pcall(f.CreateAnimationGroup, f) if ok and g and g.CreateAnimation then local ok2 = pcall(function() local a = {} for i = 1, 4 do a[i] = g:CreateAnimation("Alpha") if not (a[i].SetFromAlpha and a[i].SetToAlpha) then error("keine Alpha-API") end a[i]:SetOrder(i) end if a[1].SetSmoothing then a[1]:SetSmoothing("OUT"); a[2]:SetSmoothing("IN") a[3]:SetSmoothing("OUT"); a[4]:SetSmoothing("IN") end g:SetLooping("NONE") g:SetScript("OnFinished", function() f:SetAlpha(0); f:Hide() end) stufen = a W.anim = a -- W6-Erbe: die Dauer bleibt von aussen umstellbar end) if ok2 then ag = g end end end end -- Die vier Abschnitte auf eine Dauer und eine Schlagzahl setzen. -- 1 Schlag: 0 -> 1 -> 0, die beiden hinteren Abschnitte auf 0 s. -- 2 Schlaege: 0 -> 1 -> TAL -> 1 -> 0. W.dauerJetzt, W.schlaegeJetzt = nil, nil function W.kurveSetzen(sek, schlaege) sek = tonumber(sek) or W.DAUER if sek > 1 then sek = 1 end -- Standzeit <= 1 s, harte Zusage schlaege = (tonumber(schlaege) == 2) and 2 or 1 if sek == W.dauerJetzt and schlaege == W.schlaegeJetzt then return true end if not (stufen and stufen[1] and stufen[1].SetDuration) then return false end local ok = pcall(function() if schlaege == 2 then local t = W.DOPPEL_TAL local d = sek / 4 stufen[1]:SetFromAlpha(0); stufen[1]:SetToAlpha(1); stufen[1]:SetDuration(d * 0.8) stufen[2]:SetFromAlpha(1); stufen[2]:SetToAlpha(t); stufen[2]:SetDuration(d * 0.7) stufen[3]:SetFromAlpha(t); stufen[3]:SetToAlpha(1); stufen[3]:SetDuration(d * 0.7) stufen[4]:SetFromAlpha(1); stufen[4]:SetToAlpha(0); stufen[4]:SetDuration(d * 1.8) else stufen[1]:SetFromAlpha(0); stufen[1]:SetToAlpha(1); stufen[1]:SetDuration(sek * 0.30) stufen[2]:SetFromAlpha(1); stufen[2]:SetToAlpha(0); stufen[2]:SetDuration(sek * 0.70) stufen[3]:SetFromAlpha(0); stufen[3]:SetToAlpha(0); stufen[3]:SetDuration(0) stufen[4]:SetFromAlpha(0); stufen[4]:SetToAlpha(0); stufen[4]:SetDuration(0) end end) if ok then W.dauerJetzt, W.schlaegeJetzt = sek, schlaege end return ok end -- W6-Erbe: die alte Schnittstelle bleibt, damit Sinne/Welle6.lua nichts wissen muss. function W.dauerSetzen(sek) return W.kurveSetzen(sek, W.schlaegeJetzt or 1) end -- Rueckfall ohne Animations-API: die Kurve in Schritten ueber ns.Compat.After. Kein OnUpdate, -- kein Ticker mit 0 s - beides steht im Kontrakt. W.SCHRITTE = 8 local function pulsOhneAnimation(sek, schlaege) local n = W.SCHRITTE local dt = sek / n f:SetAlpha(0) f:Show() W.lauf = (W.lauf or 0) + 1 local meinLauf = W.lauf for i = 1, n do ns.Compat.After(dt * i, function() if W.lauf ~= meinLauf then return end local p = i / n local a if schlaege == 2 then -- zwei Buckel ueber die Gesamtdauer, der zweite etwas kuerzer a = (p < 0.4) and (p / 0.4) or (p < 0.55) and (1 - (p - 0.4) / 0.15 * (1 - W.DOPPEL_TAL)) or (p < 0.7) and (W.DOPPEL_TAL + (p - 0.55) / 0.15 * (1 - W.DOPPEL_TAL)) or (1 - (p - 0.7) / 0.3) else a = (p < 0.3) and (p / 0.3) or (1 - (p - 0.3) / 0.7) end if a < 0 then a = 0 elseif a > 1 then a = 1 end f:SetAlpha(a) if i == n then f:SetAlpha(0); f:Hide() end end) end return true end -- W7: Der Barrierefrei-Modus HEBT die Spitze an (Faktor) und laesst den Puls etwas laenger -- stehen. Der Riegel dazwischen wird dafuer haerter - das ist die Photosensibilitaets-Bremse. W.BF_DAUER = 1.0 W.BF_FAKTOR = 1.25 W.BF_RIEGEL = 6 local function barrierefrei() local W6 = ns.Welle6 return (W6 and W6.an and W6.an("barrierefrei")) and true or false end W.barrierefrei = barrierefrei local function erlaubt() if not ns.Get("glow") then return false end -- W7: Streamer-Modus laesst das Gluehen aus. Ein Vollbild-Puls ist im Stream fuer die -- Zuschauer, nicht fuer den Spieler. if ns.Get("streamer") then return false end if ns.Regie and ns.Regie.ladeRiegelBis and GetTime() < ns.Regie.ladeRiegelBis then return false end if UnitIsDeadOrGhost and UnitIsDeadOrGhost("player") then return false end local riegel = barrierefrei() and W.BF_RIEGEL or W.RIEGEL if GetTime() - (W.zuletzt or 0) < riegel then return false end -- Photosensibilitaet return true end W.erlaubt = erlaubt -- W.pulsStufe(stufe, farbe): der Weg, den ns.nachAusgabe geht. farbe uebersteuert die Stufenfarbe -- (Atem bleibt blau). Rueckgabe true, wenn wirklich ein Puls losgelaufen ist. function W.pulsStufe(stufe, farbe) stufe = tonumber(stufe) or 3 if stufe < 1 then return false end if stufe > 3 then stufe = 3 end local s = W.STUFEN[stufe] or W.STUFEN[3] return W.puls(s.spitze, farbe or s.farbe, s.dauer, s.schlaege) end -- W.puls(spitze, farbe [, dauer, schlaege]): einmaliger Puls. Die alte Zwei-Argument-Form bleibt. function W.puls(spitze, farbe, dauer, schlaege) if not erlaubt() then return false end spitze = tonumber(spitze) or W.SPITZE dauer = tonumber(dauer) or W.DAUER schlaege = (tonumber(schlaege) == 2) and 2 or 1 if barrierefrei() then spitze = spitze * W.BF_FAKTOR if dauer < W.BF_DAUER then dauer = W.BF_DAUER end end if dauer > 1 then dauer = 1 end if spitze > W.SPITZE_MAX then spitze = W.SPITZE_MAX elseif spitze < 0 then spitze = 0 end faerbe(farbe) -- Rueckfall (vier harte Baender): gedeckelt, weil eine harte Flaeche staerker wirkt als ein -- Verlauf - Photosensibilitaet, Recherche 02 2.3. if W.hartesBand and spitze > W.SPITZE_HART then spitze = W.SPITZE_HART end bemesse(spitze) W.spitzeJetzt, W.zuletzt = spitze, GetTime() if ag and W.kurveSetzen(dauer, schlaege) then local ok = pcall(function() if ag:IsPlaying() then ag:Stop() end f:SetAlpha(0) f:Show() ag:Play() end) if ok then W.wegJetzt = "anim"; return true end end W.wegJetzt = "schritte" return pulsOhneAnimation(dauer, schlaege) end function W.aus() W.lauf = (W.lauf or 0) + 1 -- laufende Schritt-Kette entwerten if ag and ag.IsPlaying and ag:IsPlaying() then pcall(ag.Stop, ag) end f:SetAlpha(0) f:Hide() end ns.nachAusgabe(function(id, e) if not e then return end -- DESIGN-V3 A-5: die Verzweigung haengt an der STUFE, nicht an der Klasse. local stufe = tonumber(e.stufe) or ((e.klasse == "warn") and 2 or 0) if stufe < 1 then return end -- W7: Stufe 1 und 2 pulsen jetzt ebenfalls - dezent. Bis 0.10.0 war der Bildschirmrand ein -- Ja/Nein-Kanal (nur Stufe 3); drei unterscheidbare Stufen tragen mehr und stoeren weniger, -- weil die unteren beiden deutlich schwaecher sind als der alte Einheitspuls. -- Atem-Alarm bleibt blau (Wasser ist blau, das ist gelernt). local farbe = (type(id) == "string" and id:find("^ATEM")) and "blau" or nil W.pulsStufe(stufe, farbe) end) -- Sicherheitsnetz: Tod/Ladebildschirm brechen einen laufenden Puls ab ns.on("PLAYER_DEAD", W.aus) ns.on("PLAYER_ENTERING_WORLD", W.aus) function W.stand() return W.weich, W.hartesBand, W.wegJetzt, W.spitzeJetzt, W.farbeJetzt, W.dauerJetzt, W.schlaegeJetzt end