diff --git a/EllesmereUI.toc b/EllesmereUI.toc index 1033dd9c..b59f2b35 100644 --- a/EllesmereUI.toc +++ b/EllesmereUI.toc @@ -25,6 +25,7 @@ EllesmereUI_Startup.lua # Shared EllesmereUI Files EllesmereUI.lua +EllesmereUI_Kick.lua EllesmereUI_Widgets.lua EllesmereUI_Visibility.lua EllesmereUI_Presets.lua diff --git a/EllesmereUINameplates/EllesmereUINameplates.lua b/EllesmereUINameplates/EllesmereUINameplates.lua index d28e985b..7be8223c 100644 --- a/EllesmereUINameplates/EllesmereUINameplates.lua +++ b/EllesmereUINameplates/EllesmereUINameplates.lua @@ -1834,55 +1834,20 @@ local function InitDB() -- Legacy stub: NewDB + DeepMergeDefaults handles defaults now. -- Kept as a no-op so any stray call sites don't error. end -local kickSpellsByClass = { - DEATHKNIGHT = {47528}, - WARRIOR = {6552}, - WARLOCK = {19647, 89766, 119910, 1276467, 132409}, - SHAMAN = {57994}, - ROGUE = {1766}, - PRIEST = {15487}, - PALADIN = {31935, 96231}, - MONK = {116705}, - MAGE = {2139}, - HUNTER = {187707, 147362}, - EVOKER = {351338}, - DRUID = {38675, 78675, 106839}, - DEMONHUNTER = {183752}, -} -local activeKickSpell -function ns.GetActiveKickSpell() return activeKickSpell end -local function RefreshKickAbility() - local playerClass = UnitClassBase("player") - local classKicks = kickSpellsByClass[playerClass] - activeKickSpell = nil - if not classKicks then return end - for i = 1, #classKicks do - local spellId = classKicks[i] - if C_SpellBook and C_SpellBook.IsSpellKnownOrInSpellBook then - local known = C_SpellBook.IsSpellKnownOrInSpellBook(spellId) - local petKnown = Enum and Enum.SpellBookSpellBank and C_SpellBook.IsSpellKnownOrInSpellBook(spellId, Enum.SpellBookSpellBank.Pet) - if known or petKnown then activeKickSpell = spellId end - elseif IsSpellKnown and IsSpellKnown(spellId) then - activeKickSpell = spellId - end - end -end -local function ComputeCastBarTint(readyTint, baseTint) - if not activeKickSpell then return baseTint.r, baseTint.g, baseTint.b end - if not (C_Spell and C_Spell.GetSpellCooldownDuration) then return baseTint.r, baseTint.g, baseTint.b end - if not (C_CurveUtil and C_CurveUtil.EvaluateColorValueFromBoolean) then return baseTint.r, baseTint.g, baseTint.b end - local cdTime = C_Spell.GetSpellCooldownDuration(activeKickSpell) - if not (cdTime and cdTime.IsZero) then return baseTint.r, baseTint.g, baseTint.b end - local offCooldown = cdTime:IsZero() - local rVal = C_CurveUtil.EvaluateColorValueFromBoolean(offCooldown, baseTint.r, readyTint.r) - local gVal = C_CurveUtil.EvaluateColorValueFromBoolean(offCooldown, baseTint.g, readyTint.g) - local bVal = C_CurveUtil.EvaluateColorValueFromBoolean(offCooldown, baseTint.b, readyTint.b) - return rVal, gVal, bVal -end --- Exposed for the cast overlay file (EllesmereUINameplates_CastOverlay.lua) --- so the overlay bar can apply the same interrupt-ready tint as the on-plate --- cast bar without duplicating the logic. -ns.ComputeCastBarTint = ComputeCastBarTint +function ns.GetActiveKickSpell() + return EllesmereUI and EllesmereUI.GetActiveKickSpell and EllesmereUI.GetActiveKickSpell() +end +-- Cast overlay uses the same tint as the on-plate cast bar. +ns.ComputeCastBarTint = function(readyTint, baseTint) + if EllesmereUI and EllesmereUI.ComputeCastBarTint then + return EllesmereUI.ComputeCastBarTint(readyTint, baseTint) + end + return baseTint.r, baseTint.g, baseTint.b +end +local function GetActiveKickSpell() + return ns.GetActiveKickSpell() +end +local ComputeCastBarTint = ns.ComputeCastBarTint function ns.RefreshBorder() -- Bump appearance gen so pooled/off-screen plates pick up the -- change on their next SetUnit (cache-hit re-spawns check this). @@ -1978,8 +1943,6 @@ function ns.RefreshAllSettings() if ns.ApplyClassPowerSetting then ns.ApplyClassPowerSetting() end end local kickWatcher = CreateFrame("Frame") -kickWatcher:RegisterEvent("PLAYER_LOGIN") -kickWatcher:RegisterEvent("SPELLS_CHANGED") local activeCastCount = 0 -- PERF: set of plates currently casting so kick/color updates iterate only -- the 1-3 casting plates instead of all 20+ plates in the scene. @@ -2008,8 +1971,6 @@ kickWatcher:SetScript("OnEvent", function(self, event) end end end - else - RefreshKickAbility() end end) local _castColorTicker @@ -2019,7 +1980,7 @@ local function NotifyCastStarted(plate) if activeCastCount == 1 then kickWatcher:RegisterEvent("SPELL_UPDATE_COOLDOWN") kickWatcher:RegisterEvent("SPELL_UPDATE_USABLE") - if activeKickSpell and not _castColorTicker then + if GetActiveKickSpell() and not _castColorTicker then _castColorTicker = C_Timer.NewTicker(0.2, function() for pl in pairs(ns._castingPlates) do if pl.isCasting and pl.unit and pl._kickProtected ~= nil then @@ -4829,7 +4790,7 @@ function NameplateFrame:HideKickTick() end end function NameplateFrame:UpdateKickTick(kickProtected, isChannel, isEmpowered) - if not GetKickTickEnabled() or not activeKickSpell then + if not GetKickTickEnabled() or not GetActiveKickSpell() then self:HideKickTick() return end @@ -4858,7 +4819,7 @@ function NameplateFrame:UpdateKickTick(kickProtected, isChannel, isEmpowered) return end local totalDur = castDuration:GetTotalDuration() - local interruptCD = C_Spell.GetSpellCooldownDuration(activeKickSpell) + local interruptCD = C_Spell.GetSpellCooldownDuration(GetActiveKickSpell()) if not interruptCD then self:HideKickTick() return @@ -4925,14 +4886,14 @@ function NameplateFrame:UpdateKickTick(kickProtected, isChannel, isEmpowered) -- activeKickSpell can go nil mid-cast if a spec/talent change -- fires SPELLS_CHANGED and the new spec doesn't have a kick -- learned. Bail rather than pass nil to C_Spell. - if not activeKickSpell then + if not GetActiveKickSpell() then self:HideKickTick() return end -- Compute tick visibility: show only when kick is on CD AND cast is interruptible. -- Both are secret booleans chain EvaluateColorValueFromBoolean calls -- to combine conditions into a single secret alpha. - local icd = C_Spell.GetSpellCooldownDuration(activeKickSpell) + local icd = C_Spell.GetSpellCooldownDuration(GetActiveKickSpell()) if icd and icd.IsZero and C_CurveUtil and C_CurveUtil.EvaluateColorValueFromBoolean then local interruptible = C_CurveUtil.EvaluateColorValueFromBoolean(self._kickProtected, 0, 1) local kickReady = icd:IsZero() diff --git a/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua b/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua index 0a044650..2f0808ed 100644 --- a/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua +++ b/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua @@ -2990,6 +2990,8 @@ initFrame:SetScript("OnEvent", function(self) showCastDuration = { player=true, target=true, focus=true }, showCastTarget = { player=true, target=true, focus=true }, castbarFillColor = { player=true, target=true, focus=true }, + castbarInterruptReadyColor = { target=true, focus=true }, + castbarKickTickEnabled = { target=true, focus=true }, showClassPowerBar = { player=true }, lockClassPowerToFrame= { player=true }, classPowerStyle = { player=true }, @@ -5253,48 +5255,70 @@ initFrame:SetScript("OnEvent", function(self) disabled=cbhDis, disabledTooltip=cbhTip, rawTooltip=cbhRaw, getValue=GetCastbarHeight, setValue=function(v) SetCastbarHeight(v); ReloadAndUpdate(); UpdatePreview() end }); y = y - h - -- Inline fill color swatch on Show Cast Bar + -- Inline cast color swatch(es) on Show Cast Bar do local leftRgn = sharedCastRow1._leftRegion - local cbSw = EllesmereUI.BuildColorSwatch(leftRgn, leftRgn:GetFrameLevel() + 5, - function() - local c = SGetSupported("castbarFillColor") - c = c or { r=1, g=0.7, b=0 } - return c.r, c.g, c.b, 1 - end, - function(r, g, b) - UNIT_DB_MAP[selectedUnit]().castbarFillColor = { r=r, g=g, b=b } - ReloadAndUpdate(); UpdatePreview() - end, false, 20) - cbSw:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -12, 0) - cbSw:SetScript("OnEnter", function(self) EllesmereUI.ShowWidgetTooltip(self, "Fill Color") end) - cbSw:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) - leftRgn._lastInline = cbSw + local function AddCastColorSwatch(tooltip, colorKey, fallback) + local sw = EllesmereUI.BuildColorSwatch(leftRgn, leftRgn:GetFrameLevel() + 5, + function() + local c = SGetSupported(colorKey) + c = c or fallback + return c.r, c.g, c.b, 1 + end, + function(r, g, b) + SSetSupported(colorKey, { r = r, g = g, b = b }) + ReloadAndUpdate(); UpdatePreview() + end, false, 20) + sw:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -12, 0) + sw:SetScript("OnEnter", function(self) EllesmereUI.ShowWidgetTooltip(self, tooltip) end) + sw:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + leftRgn._lastInline = sw + end + if selectedUnit == "target" or selectedUnit == "focus" then + -- Inline swatches anchor right-to-left; add CD first so interruptible sits left of it + AddCastColorSwatch("Interrupt on CD", "castbarInterruptReadyColor", { r = 0.92, g = 0.35, b = 0.20 }) + AddCastColorSwatch("Interruptible Cast", "castbarFillColor", { r = 0.863, g = 0.820, b = 0.639 }) + else + AddCastColorSwatch("Fill Color", "castbarFillColor", { r = 1, g = 0.7, b = 0 }) + end end -- Sync icon: Show Cast Bar + Fill Color (left region) do local rgn = sharedCastRow1._leftRegion + local isKickUnit = selectedUnit == "target" or selectedUnit == "focus" EllesmereUI.BuildSyncIcon({ region = rgn, - tooltip = "Apply Show Cast Bar and Fill Color to all Frames", + tooltip = isKickUnit and "Apply Show Cast Bar and Cast Color to Target and Focus" + or "Apply Show Cast Bar and Fill Color to all Frames", onClick = function() local v = GetCastbarEnabled(selectedUnit) local c = UNIT_DB_MAP[selectedUnit]().castbarFillColor - for _, key in ipairs(GROUP_UNIT_ORDER) do + local readyC = isKickUnit and UNIT_DB_MAP[selectedUnit]().castbarInterruptReadyColor + local keys = isKickUnit and { "target", "focus" } or GROUP_UNIT_ORDER + for _, key in ipairs(keys) do SetCastbarEnabled(key, v) - if c then UNIT_DB_MAP[key]().castbarFillColor = { r=c.r, g=c.g, b=c.b } end + if c then UNIT_DB_MAP[key]().castbarFillColor = { r = c.r, g = c.g, b = c.b } end + if readyC then UNIT_DB_MAP[key]().castbarInterruptReadyColor = { r = readyC.r, g = readyC.g, b = readyC.b } end end ReloadAndUpdate(); EllesmereUI:RefreshPage() end, isSynced = function() local v = GetCastbarEnabled(selectedUnit) local c = UNIT_DB_MAP[selectedUnit]().castbarFillColor - for _, key in ipairs(GROUP_UNIT_ORDER) do + local readyC = isKickUnit and UNIT_DB_MAP[selectedUnit]().castbarInterruptReadyColor + local keys = isKickUnit and { "target", "focus" } or GROUP_UNIT_ORDER + for _, key in ipairs(keys) do if GetCastbarEnabled(key) ~= v then return false end local kc = UNIT_DB_MAP[key]().castbarFillColor if c and kc then if kc.r ~= c.r or kc.g ~= c.g or kc.b ~= c.b then return false end elseif c ~= kc then return false end + if isKickUnit then + local kr = UNIT_DB_MAP[key]().castbarInterruptReadyColor + if readyC and kr then + if kr.r ~= readyC.r or kr.g ~= readyC.g or kr.b ~= readyC.b then return false end + elseif readyC ~= kr then return false end + end end return true end, @@ -5306,9 +5330,13 @@ initFrame:SetScript("OnEvent", function(self) onApply = function(checkedKeys) local v = GetCastbarEnabled(selectedUnit) local c = UNIT_DB_MAP[selectedUnit]().castbarFillColor + local readyC = isKickUnit and UNIT_DB_MAP[selectedUnit]().castbarInterruptReadyColor for _, key in ipairs(checkedKeys) do SetCastbarEnabled(key, v) - if c then UNIT_DB_MAP[key]().castbarFillColor = { r=c.r, g=c.g, b=c.b } end + if c then UNIT_DB_MAP[key]().castbarFillColor = { r = c.r, g = c.g, b = c.b } end + if readyC and (key == "target" or key == "focus") then + UNIT_DB_MAP[key]().castbarInterruptReadyColor = { r = readyC.r, g = readyC.g, b = readyC.b } + end end ReloadAndUpdate(); EllesmereUI:RefreshPage() end, @@ -5356,7 +5384,7 @@ initFrame:SetScript("OnEvent", function(self) }) end - -- Row 2: Show Icon | Hide When Idle + -- Row 2: Show Icon | Hide When Idle (or kick tick for target/focus) local function GetShowIcon() if selectedUnit == "player" then local v = UNIT_DB_MAP.player().showPlayerCastIcon @@ -5384,14 +5412,37 @@ initFrame:SetScript("OnEvent", function(self) UNIT_DB_MAP[selectedUnit]().castbarHideWhenInactive = val end + local isKickCastUnit = selectedUnit == "target" or selectedUnit == "focus" + local castRow2Right + if isKickCastUnit then + castRow2Right = { + type = "toggle", + text = "Show Tick at Kick Ready Spot", + tooltip = "Shows a small white tick mark on the cast bar at the point where the cast will be when your interrupt comes off cooldown.", + getValue = function() + local v = SGetSupported("castbarKickTickEnabled") + if v == nil then return true end + return v + end, + setValue = function(v) + SSetSupported("castbarKickTickEnabled", v) + ReloadAndUpdate(); UpdatePreview() + end, + } + else + castRow2Right = { + type = "toggle", + text = "Hide When Idle", + getValue = GetHideInactive, + setValue = function(v) SetHideInactive(v); ReloadAndUpdate(); UpdatePreview() end, + } + end local castRow2 castRow2, h = W:DualRow(parent, y, { type="toggle", text="Show Icon", getValue=GetShowIcon, setValue=function(v) SetShowIcon(v); ReloadAndUpdate(); UpdatePreview() end }, - { type="toggle", text="Hide When Idle", - getValue=GetHideInactive, - setValue=function(v) SetHideInactive(v); ReloadAndUpdate(); UpdatePreview() end }); y = y - h + castRow2Right); y = y - h -- Sync icon: Show Icon (left) do local rgn = castRow2._leftRegion @@ -5437,9 +5488,34 @@ initFrame:SetScript("OnEvent", function(self) }, }) end - -- Sync icon: Hide When Idle (right) + -- Sync icon: Hide When Idle or kick tick (right) do local rgn = castRow2._rightRegion + if isKickCastUnit then + EllesmereUI.BuildSyncIcon({ + region = rgn, + tooltip = "Apply Kick Tick Setting to Target and Focus", + onClick = function() + local v = UNIT_DB_MAP[selectedUnit]().castbarKickTickEnabled + for _, key in ipairs({ "target", "focus" }) do + UNIT_DB_MAP[key]().castbarKickTickEnabled = v + end + ReloadAndUpdate(); EllesmereUI:RefreshPage() + end, + isSynced = function() + local v = UNIT_DB_MAP[selectedUnit]().castbarKickTickEnabled + for _, key in ipairs({ "target", "focus" }) do + if key ~= selectedUnit then + local kv = UNIT_DB_MAP[key]().castbarKickTickEnabled + if (v == nil) ~= (kv == nil) then return false end + if v ~= nil and kv ~= nil and v ~= kv then return false end + end + end + return true + end, + flashTargets = function() return { rgn } end, + }) + else EllesmereUI.BuildSyncIcon({ region = rgn, tooltip = "Apply Hide When Idle to all Frames", @@ -5473,6 +5549,53 @@ initFrame:SetScript("OnEvent", function(self) end, }, }) + end + end + + if isKickCastUnit then + local castHideRow + castHideRow, h = W:DualRow(parent, y, + { type="toggle", text="Hide When Idle", + getValue=GetHideInactive, + setValue=function(v) SetHideInactive(v); ReloadAndUpdate(); UpdatePreview() end }, + { text="" }) + y = y - h + do + local rgn = castHideRow._leftRegion + EllesmereUI.BuildSyncIcon({ + region = rgn, + tooltip = "Apply Hide When Idle to all Frames", + onClick = function() + local v = GetHideInactive() + for _, key in ipairs(GROUP_UNIT_ORDER) do + UNIT_DB_MAP[key]().castbarHideWhenInactive = v + end + ReloadAndUpdate(); EllesmereUI:RefreshPage() + end, + isSynced = function() + local v = GetHideInactive() + for _, key in ipairs(GROUP_UNIT_ORDER) do + local kv = UNIT_DB_MAP[key]().castbarHideWhenInactive + if kv == nil then kv = true end + if kv ~= v then return false end + end + return true + end, + flashTargets = function() return { rgn } end, + multiApply = { + elementKeys = GROUP_UNIT_ORDER, + elementLabels = SHORT_LABELS, + getCurrentKey = function() return selectedUnit end, + onApply = function(checkedKeys) + local v = GetHideInactive() + for _, key in ipairs(checkedKeys) do + UNIT_DB_MAP[key]().castbarHideWhenInactive = v + end + ReloadAndUpdate(); EllesmereUI:RefreshPage() + end, + }, + }) + end end -- Row 3: Spell Name Size (with inline color swatch) | Duration Size (with inline color swatch) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua index 1208ad29..d84da0fa 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua @@ -291,6 +291,8 @@ local defaults = { showCastDuration = true, showCastTarget = true, castbarFillColor = { r = 0.863, g = 0.820, b = 0.639 }, + castbarInterruptReadyColor = { r = 0.92, g = 0.35, b = 0.20 }, + castbarKickTickEnabled = true, castbarClassColored = false, healthDisplay = "both", showBuffs = true, @@ -542,6 +544,8 @@ local defaults = { showCastDuration = true, showCastTarget = true, castbarFillColor = { r = 0.863, g = 0.820, b = 0.639 }, + castbarInterruptReadyColor = { r = 0.92, g = 0.35, b = 0.20 }, + castbarKickTickEnabled = true, castbarClassColored = false, healthDisplay = "perhp", leftTextContent = "name", @@ -2874,6 +2878,228 @@ end -- ApplyCastbarUnlockPos removed: cast bar positioning is now fully owned -- by the centralized unlock/anchor system (ApplySavedPositions). +local function GetActiveKickSpell() + return EllesmereUI and EllesmereUI.GetActiveKickSpell and EllesmereUI.GetActiveKickSpell() +end +local function ComputeCastBarTint(readyTint, baseTint) + if EllesmereUI and EllesmereUI.ComputeCastBarTint then + return EllesmereUI.ComputeCastBarTint(readyTint, baseTint) + end + return baseTint.r, baseTint.g, baseTint.b +end +local function IsKickCastbarUnit(unit) + return unit == "target" or unit == "focus" +end +local function GetCastbarKickTickEnabled(settings) + if not settings then return true end + if settings.castbarKickTickEnabled ~= nil then return settings.castbarKickTickEnabled end + return true +end +local function GetCastbarUninterruptible(castbar) + local v = castbar and castbar.notInterruptible + if type(v) == "nil" then return false end + return v +end +local function HideUnitFrameKickTick(castbar) + if not castbar or not castbar.kickPositioner then return end + castbar.kickPositioner:Hide() + castbar.kickMarker:Hide() + if castbar._kickTicker then + castbar._kickTicker:Cancel() + castbar._kickTicker = nil + end +end +local function ApplyUnitFrameCastColor(castbar) + if not castbar or not castbar.castTintLayer then return end + local settings = castbar._eufSettings + local ownerUnit = castbar.__owner and castbar.__owner.unit + local cc + if settings and settings.castbarClassColored and ownerUnit == "player" then + if ownerUnit then + local _, classToken = UnitClass(ownerUnit) + if classToken and EllesmereUI.GetClassColor then + cc = EllesmereUI.GetClassColor(classToken) + end + end + end + if not cc then + local baseTint = (settings and settings.castbarFillColor) or GetCastbarColor() + if IsKickCastbarUnit(ownerUnit) then + local readyTint = (settings and settings.castbarInterruptReadyColor) or { r = 0.92, g = 0.35, b = 0.20 } + local cr, cg, cb = ComputeCastBarTint(readyTint, baseTint) + cc = { r = cr, g = cg, b = cb } + else + cc = baseTint + end + end + castbar.castTintLayer:SetVertexColor(cc.r, cc.g, cc.b) + if castbar._shieldedTint then + local uninterruptible = GetCastbarUninterruptible(castbar) + if castbar._shieldedTint.SetAlphaFromBoolean then + castbar._shieldedTint:SetAlphaFromBoolean(uninterruptible, 1, 0) + else + castbar._shieldedTint:SetAlpha(uninterruptible and 1 or 0) + end + end +end +local function UpdateUnitFrameKickTick(castbar) + if not castbar or not castbar.kickPositioner then return end + local settings = castbar._eufSettings + local ownerUnit = castbar.__owner and castbar.__owner.unit + if not IsKickCastbarUnit(ownerUnit) then + HideUnitFrameKickTick(castbar) + return + end + if not GetCastbarKickTickEnabled(settings) or not GetActiveKickSpell() then + HideUnitFrameKickTick(castbar) + return + end + if not (C_Spell and C_Spell.GetSpellCooldownDuration) then + HideUnitFrameKickTick(castbar) + return + end + local kickProtected = GetCastbarUninterruptible(castbar) + castbar._kickProtected = kickProtected + local isChannel = castbar.channeling and true or false + local isEmpowered = false + if not (UnitCastingDuration and ownerUnit) then + HideUnitFrameKickTick(castbar) + return + end + local castDuration + if isChannel then + if UnitEmpoweredChannelDuration then + castDuration = UnitEmpoweredChannelDuration(ownerUnit, true) + if castDuration then isEmpowered = true end + end + if not castDuration and UnitChannelDuration then + castDuration = UnitChannelDuration(ownerUnit) + end + else + castDuration = UnitCastingDuration(ownerUnit) + end + if not castDuration then + HideUnitFrameKickTick(castbar) + return + end + local totalDur = castDuration:GetTotalDuration() + local interruptCD = C_Spell.GetSpellCooldownDuration(GetActiveKickSpell()) + if not interruptCD then + HideUnitFrameKickTick(castbar) + return + end + local barW = castbar:GetWidth() + local barH = castbar:GetHeight() + if not barW or barW <= 0 then + HideUnitFrameKickTick(castbar) + return + end + castbar.kickPositioner:SetSize(barW, barH) + castbar.kickPositioner:SetMinMaxValues(0, totalDur) + castbar.kickMarker:SetMinMaxValues(0, totalDur) + castbar.kickMarker:SetSize(barW, barH) + castbar.kickPositioner:SetValue(castDuration:GetElapsedDuration()) + castbar.kickMarker:SetValue(interruptCD:GetRemainingDuration()) + castbar.kickTick:SetColorTexture(1, 1, 1, 1) + if isChannel and not isEmpowered then + castbar.kickPositioner:SetFillStyle(Enum.StatusBarFillStyle.Reverse) + castbar.kickMarker:SetFillStyle(Enum.StatusBarFillStyle.Reverse) + castbar.kickMarker:ClearAllPoints() + castbar.kickTick:ClearAllPoints() + castbar.kickMarker:SetPoint("RIGHT", castbar.kickPositioner:GetStatusBarTexture(), "LEFT") + castbar.kickTick:SetPoint("TOP", castbar.kickMarker, "TOP", 0, 0) + castbar.kickTick:SetPoint("BOTTOM", castbar.kickMarker, "BOTTOM", 0, 0) + castbar.kickTick:SetPoint("RIGHT", castbar.kickMarker:GetStatusBarTexture(), "LEFT") + else + castbar.kickPositioner:SetFillStyle(Enum.StatusBarFillStyle.Standard) + castbar.kickMarker:SetFillStyle(Enum.StatusBarFillStyle.Standard) + castbar.kickMarker:ClearAllPoints() + castbar.kickTick:ClearAllPoints() + castbar.kickMarker:SetPoint("LEFT", castbar.kickPositioner:GetStatusBarTexture(), "RIGHT") + castbar.kickTick:SetPoint("TOP", castbar.kickMarker, "TOP", 0, 0) + castbar.kickTick:SetPoint("BOTTOM", castbar.kickMarker, "BOTTOM", 0, 0) + castbar.kickTick:SetPoint("LEFT", castbar.kickMarker:GetStatusBarTexture(), "RIGHT") + end + castbar.kickPositioner:Show() + castbar.kickMarker:Show() + if interruptCD.IsZero and C_CurveUtil and C_CurveUtil.EvaluateColorValueFromBoolean then + local interruptible = C_CurveUtil.EvaluateColorValueFromBoolean(kickProtected, 0, 1) + local kickReady = interruptCD:IsZero() + local alpha = C_CurveUtil.EvaluateColorValueFromBoolean(kickReady, 0, interruptible) + castbar.kickTick:SetAlpha(alpha) + else + castbar.kickTick:SetAlpha(0) + end + if castbar._kickTicker then castbar._kickTicker:Cancel() end + castbar._kickTicker = C_Timer.NewTicker(0.1, function() + if not castbar:IsShown() or not ownerUnit then + HideUnitFrameKickTick(castbar) + return + end + if not GetActiveKickSpell() then + HideUnitFrameKickTick(castbar) + return + end + local icd = C_Spell.GetSpellCooldownDuration(GetActiveKickSpell()) + if icd and icd.IsZero and C_CurveUtil and C_CurveUtil.EvaluateColorValueFromBoolean then + local interruptible = C_CurveUtil.EvaluateColorValueFromBoolean(castbar._kickProtected, 0, 1) + local kickReady = icd:IsZero() + local alpha = C_CurveUtil.EvaluateColorValueFromBoolean(kickReady, 0, interruptible) + castbar.kickTick:SetAlpha(alpha) + end + end) +end + +ns._castingCastbars = {} +local activeCastbarCount = 0 +local _ufCastColorTicker +local ufKickWatcher = CreateFrame("Frame") +ufKickWatcher:SetScript("OnEvent", function(_, event) + if event == "SPELL_UPDATE_COOLDOWN" or event == "SPELL_UPDATE_USABLE" then + for cb in pairs(ns._castingCastbars) do + if cb:IsShown() and cb.__owner and cb.__owner.unit then + ApplyUnitFrameCastColor(cb) + UpdateUnitFrameKickTick(cb) + end + end + end +end) +local function NotifyCastbarStarted(castbar) + if not castbar or not castbar.__owner then return end + if not IsKickCastbarUnit(castbar.__owner.unit) then return end + if ns._castingCastbars[castbar] then return end + ns._castingCastbars[castbar] = true + activeCastbarCount = activeCastbarCount + 1 + if activeCastbarCount == 1 then + ufKickWatcher:RegisterEvent("SPELL_UPDATE_COOLDOWN") + ufKickWatcher:RegisterEvent("SPELL_UPDATE_USABLE") + if GetActiveKickSpell() and not _ufCastColorTicker then + _ufCastColorTicker = C_Timer.NewTicker(0.2, function() + for cb in pairs(ns._castingCastbars) do + if cb:IsShown() then + ApplyUnitFrameCastColor(cb) + end + end + end) + end + end +end +local function NotifyCastbarEnded(castbar) + if not castbar or not ns._castingCastbars[castbar] then return end + ns._castingCastbars[castbar] = nil + activeCastbarCount = activeCastbarCount - 1 + if activeCastbarCount <= 0 then + activeCastbarCount = 0 + wipe(ns._castingCastbars) + ufKickWatcher:UnregisterEvent("SPELL_UPDATE_COOLDOWN") + ufKickWatcher:UnregisterEvent("SPELL_UPDATE_USABLE") + if _ufCastColorTicker then + _ufCastColorTicker:Cancel() + _ufCastColorTicker = nil + end + end +end + local function CreateCastBar(frame, unit, settings) local settings = GetSettingsForUnit(unit) @@ -3019,33 +3245,48 @@ local function CreateCastBar(frame, unit, settings) shieldedTint:SetAlpha(0) castbar._shieldedTint = shieldedTint - castbar.PostCastStart = function(self) + local function OnCastbarCastActive(self) if self.castTintLayer then self.castTintLayer:SetAlpha(1) - local cc - local uSettings = self._eufSettings - -- Class colored only applies to the player cast bar - local ownerUnit = self.__owner and self.__owner.unit - if uSettings and uSettings.castbarClassColored and ownerUnit == "player" then - if ownerUnit then - local _, classToken = UnitClass(ownerUnit) - if classToken and EllesmereUI.GetClassColor then - cc = EllesmereUI.GetClassColor(classToken) - end - end - end - if not cc then - cc = (uSettings and uSettings.castbarFillColor) or GetCastbarColor() - end - self.castTintLayer:SetVertexColor(cc.r, cc.g, cc.b) - if self._shieldedTint then - self._shieldedTint:SetAlphaFromBoolean(self.notInterruptible, 1, 0) - end - end + ApplyUnitFrameCastColor(self) + end + end + castbar.PostCastStart = OnCastbarCastActive + castbar.PostChannelStart = OnCastbarCastActive + + castbar.PostCastInterruptible = function(self) + ApplyUnitFrameCastColor(self) + UpdateUnitFrameKickTick(self) + end + + if IsKickCastbarUnit(unit) then + local kickClip = CreateFrame("Frame", nil, castbar) + kickClip:SetAllPoints(castbar) + kickClip:SetClipsChildren(true) + castbar.kickClip = kickClip + local kickPositioner = CreateFrame("StatusBar", nil, kickClip) + kickPositioner:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8") + kickPositioner:GetStatusBarTexture():SetAlpha(0) + kickPositioner:SetPoint("CENTER", castbar) + kickPositioner:SetFrameLevel(castbar:GetFrameLevel() + 1) + kickPositioner:Hide() + castbar.kickPositioner = kickPositioner + local kickMarker = CreateFrame("StatusBar", nil, kickClip) + kickMarker:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8") + kickMarker:GetStatusBarTexture():SetAlpha(0) + kickMarker:SetPoint("LEFT", kickPositioner:GetStatusBarTexture(), "RIGHT") + kickMarker:SetSize(1, 1) + kickMarker:SetFrameLevel(castbar:GetFrameLevel() + 2) + kickMarker:Hide() + castbar.kickMarker = kickMarker + local kickTick = kickMarker:CreateTexture(nil, "OVERLAY", nil, 3) + kickTick:SetColorTexture(1, 1, 1, 1) + kickTick:SetWidth(2) + kickTick:SetPoint("TOP", kickMarker, "TOP", 0, 0) + kickTick:SetPoint("BOTTOM", kickMarker, "BOTTOM", 0, 0) + kickTick:SetPoint("LEFT", kickMarker:GetStatusBarTexture(), "RIGHT") + castbar.kickTick = kickTick end - castbar.PostChannelStart = castbar.PostCastStart - - castbar.PostCastInterruptible = castbar.PostCastStart castbar.CustomTimeText = function(self, durationObject) if self._showDuration == false then @@ -3115,6 +3356,7 @@ local function SetupShowOnCastBar(frame, unit) end local savedCastHook = castbar.PostCastStart + local savedInterruptHook = castbar.PostCastInterruptible castbar.PostCastStart = function(self, ...) local bg = self:GetParent() @@ -3167,11 +3409,17 @@ local function SetupShowOnCastBar(frame, unit) if self._layoutTextZones then self:_layoutTextZones() end end if savedCastHook then savedCastHook(self, ...) end + UpdateUnitFrameKickTick(self) + NotifyCastbarStarted(self) end castbar.PostChannelStart = castbar.PostCastStart - castbar.PostCastInterruptible = savedCastHook + castbar.PostCastInterruptible = function(self, ...) + if savedInterruptHook then savedInterruptHook(self) end + end local function dismissCastBar(self) + HideUnitFrameKickTick(self) + NotifyCastbarEnded(self) self:Hide() if self._iconFrame then self._iconFrame:Hide() end -- Read setting dynamically so changes take effect without a reload. @@ -3221,6 +3469,8 @@ local function SetupShowOnCastBar(frame, unit) -- but never fires PostCastStop, so dismissCastBar never runs and the -- background frame would otherwise remain visible as a black rectangle. castbar:HookScript("OnHide", function(self) + HideUnitFrameKickTick(self) + NotifyCastbarEnded(self) if self._iconFrame then self._iconFrame:Hide() end if shouldHideWhenInactive() then local bg = self:GetParent() @@ -6070,6 +6320,10 @@ local function ReloadFrames() tCbColor = settings.castbarFillColor end frame.Castbar:SetStatusBarColor(tCbColor.r, tCbColor.g, tCbColor.b, castbarOpacity) + if frame.Castbar:IsShown() then + ApplyUnitFrameCastColor(frame.Castbar) + UpdateUnitFrameKickTick(frame.Castbar) + end -- Apply cast bar text settings if frame.Castbar.Text then local snSz = settings.castSpellNameSize or 11 @@ -6416,6 +6670,10 @@ local function ReloadFrames() fCbColor = settings.castbarFillColor end frame.Castbar:SetStatusBarColor(fCbColor.r, fCbColor.g, fCbColor.b, castbarOpacity) + if frame.Castbar:IsShown() then + ApplyUnitFrameCastColor(frame.Castbar) + UpdateUnitFrameKickTick(frame.Castbar) + end -- Apply cast bar text settings if frame.Castbar.Text then local snSz = settings.castSpellNameSize or 11 diff --git a/EllesmereUI_Kick.lua b/EllesmereUI_Kick.lua new file mode 100644 index 00000000..ad673590 --- /dev/null +++ b/EllesmereUI_Kick.lua @@ -0,0 +1,82 @@ +-------------------------------------------------------------------------------- +-- EllesmereUI_Kick.lua +-- Shared interrupt spell lookup and cast-bar tint helpers for nameplates +-- and unit frames. +-------------------------------------------------------------------------------- + +local kickSpellsByClass = { + DEATHKNIGHT = { 47528 }, + WARRIOR = { 6552 }, + WARLOCK = { 19647, 89766, 119910, 1276467, 132409 }, + SHAMAN = { 57994 }, + ROGUE = { 1766 }, + PRIEST = { 15487 }, + PALADIN = { 31935, 96231 }, + MONK = { 116705 }, + MAGE = { 2139 }, + HUNTER = { 187707, 147362 }, + EVOKER = { 351338 }, + DRUID = { 38675, 78675, 106839 }, + DEMONHUNTER = { 183752 }, +} + +local activeKickSpell + +local function RefreshKickAbility() + local playerClass = UnitClassBase("player") + local classKicks = kickSpellsByClass[playerClass] + activeKickSpell = nil + if not classKicks then return end + for i = 1, #classKicks do + local spellId = classKicks[i] + if C_SpellBook and C_SpellBook.IsSpellKnownOrInSpellBook then + local known = C_SpellBook.IsSpellKnownOrInSpellBook(spellId) + local petKnown = Enum and Enum.SpellBookSpellBank + and C_SpellBook.IsSpellKnownOrInSpellBook(spellId, Enum.SpellBookSpellBank.Pet) + if known or petKnown then + activeKickSpell = spellId + end + elseif IsSpellKnown and IsSpellKnown(spellId) then + activeKickSpell = spellId + end + end +end + +local function ComputeCastBarTint(readyTint, baseTint) + if not activeKickSpell then + return baseTint.r, baseTint.g, baseTint.b + end + if not (C_Spell and C_Spell.GetSpellCooldownDuration) then + return baseTint.r, baseTint.g, baseTint.b + end + if not (C_CurveUtil and C_CurveUtil.EvaluateColorValueFromBoolean) then + return baseTint.r, baseTint.g, baseTint.b + end + local cdTime = C_Spell.GetSpellCooldownDuration(activeKickSpell) + if not (cdTime and cdTime.IsZero) then + return baseTint.r, baseTint.g, baseTint.b + end + local offCooldown = cdTime:IsZero() + local rVal = C_CurveUtil.EvaluateColorValueFromBoolean(offCooldown, baseTint.r, readyTint.r) + local gVal = C_CurveUtil.EvaluateColorValueFromBoolean(offCooldown, baseTint.g, readyTint.g) + local bVal = C_CurveUtil.EvaluateColorValueFromBoolean(offCooldown, baseTint.b, readyTint.b) + return rVal, gVal, bVal +end + +EllesmereUI = EllesmereUI or {} +EllesmereUI.GetActiveKickSpell = function() + return activeKickSpell +end +EllesmereUI.RefreshKickAbility = RefreshKickAbility +EllesmereUI.ComputeCastBarTint = ComputeCastBarTint + +local kickFrame = CreateFrame("Frame") +kickFrame:RegisterEvent("PLAYER_LOGIN") +kickFrame:RegisterEvent("SPELLS_CHANGED") +kickFrame:SetScript("OnEvent", function() + RefreshKickAbility() +end) + +if UnitGUID("player") then + RefreshKickAbility() +end