1396 lines
51 KiB
Lua
1396 lines
51 KiB
Lua
--------------------------------------------------------------------------------
|
||
-- Nanami-UI: Tweaks -- Ported from ShaguTweaks
|
||
-- 1. Auto Stance - auto switch warrior/druid stance on spell cast
|
||
-- 2. SuperWoW - GUID-based cast/channel data for SuperWoW client
|
||
-- 3. Turtle Compat - hide TW's overlapping target HP text, etc.
|
||
-- 4. Cooldown Numbers - show remaining cooldown time as text overlay
|
||
-- 5. Dark UI - darken the entire interface
|
||
-- 6. WorldMap Window - turn fullscreen map into a movable/scalable window
|
||
-- 7. Hunter Aspect Guard - cancel Cheetah/Pack when taking damage in combat (avoid OOC false positives)
|
||
-- 8. Mouseover Cast - cast on mouseover unit without changing target
|
||
--------------------------------------------------------------------------------
|
||
|
||
SFrames.Tweaks = SFrames.Tweaks or {}
|
||
local Tweaks = SFrames.Tweaks
|
||
|
||
SFrames.castdb = SFrames.castdb or {}
|
||
SFrames.guidToName = SFrames.guidToName or {}
|
||
SFrames.castByName = SFrames.castByName or {} -- [unitName] = { spell, start, casttime, icon, channel }
|
||
|
||
local function GetTweaksCfg()
|
||
if not SFramesDB or type(SFramesDB.Tweaks) ~= "table" then
|
||
return { autoStance = true, superWoW = true, turtleCompat = true,
|
||
cooldownNumbers = true, darkUI = false, worldMapWindow = false,
|
||
hunterAspectGuard = true, mouseoverCast = false }
|
||
end
|
||
return SFramesDB.Tweaks
|
||
end
|
||
|
||
local function strsplit(delimiter, str)
|
||
local result = {}
|
||
local pattern = "([^" .. delimiter .. "]+)"
|
||
for match in string.gfind(str, pattern) do
|
||
table.insert(result, match)
|
||
end
|
||
return unpack(result)
|
||
end
|
||
|
||
-- hooksecurefunc polyfill for WoW 1.12 (vanilla)
|
||
local _hooks = {}
|
||
local _hooksecurefunc = hooksecurefunc
|
||
if not _hooksecurefunc then
|
||
_hooksecurefunc = function(tbl, name, func)
|
||
if type(tbl) == "string" then
|
||
func = name
|
||
name = tbl
|
||
tbl = getfenv(0)
|
||
end
|
||
if not tbl or not tbl[name] then return end
|
||
local key = tostring(func)
|
||
_hooks[key] = {}
|
||
_hooks[key].old = tbl[name]
|
||
_hooks[key].new = func
|
||
tbl[name] = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||
local r1,r2,r3,r4,r5 = _hooks[key].old(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||
pcall(_hooks[key].new, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||
return r1,r2,r3,r4,r5
|
||
end
|
||
end
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Auto Stance
|
||
-- When a spell fails because you're in the wrong stance/form, automatically
|
||
-- switch to the required one. Works for warriors, druids, etc.
|
||
--------------------------------------------------------------------------------
|
||
local function InitAutoStance()
|
||
local frame = CreateFrame("Frame", "NanamiAutoStance")
|
||
local scanString = string.gsub(SPELL_FAILED_ONLY_SHAPESHIFT, "%%s", "(.+)")
|
||
|
||
frame:RegisterEvent("UI_ERROR_MESSAGE")
|
||
frame:SetScript("OnEvent", function()
|
||
for stances in string.gfind(arg1, scanString) do
|
||
for _, stance in pairs({ strsplit(",", stances) }) do
|
||
CastSpellByName(string.gsub(stance, "^%s*(.-)%s*$", "%1"))
|
||
end
|
||
end
|
||
end)
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- SuperWoW Compatibility
|
||
-- Provides GUID-based cast/channel data when SuperWoW client mod is active.
|
||
-- Data stored in SFrames.castdb[guid] for consumption by castbar features.
|
||
--------------------------------------------------------------------------------
|
||
local function InitSuperWoW()
|
||
if not SpellInfo and not UnitGUID and not SUPERWOW_VERSION then return end
|
||
|
||
local castdb = SFrames.castdb
|
||
|
||
local frame = CreateFrame("Frame", "NanamiSuperWoW")
|
||
frame:RegisterEvent("UNIT_CASTEVENT")
|
||
frame:SetScript("OnEvent", function()
|
||
if arg3 == "START" or arg3 == "CAST" or arg3 == "CHANNEL" then
|
||
local guid = arg1
|
||
local spell, icon, _
|
||
if SpellInfo and SpellInfo(arg4) then
|
||
spell, _, icon = SpellInfo(arg4)
|
||
end
|
||
spell = spell or UNKNOWN
|
||
icon = icon or "Interface\\Icons\\INV_Misc_QuestionMark"
|
||
|
||
if not castdb[guid] then castdb[guid] = {} end
|
||
castdb[guid].cast = spell
|
||
castdb[guid].rank = nil
|
||
castdb[guid].start = GetTime()
|
||
castdb[guid].casttime = arg5
|
||
castdb[guid].icon = icon
|
||
castdb[guid].channel = (arg3 == "CHANNEL") or false
|
||
|
||
-- Build GUID -> name mapping and castByName
|
||
-- Try UnitName(guid) directly (SuperWoW allows GUID as unitID)
|
||
local castName
|
||
local ok_n, gname = pcall(UnitName, guid)
|
||
if ok_n and gname and gname ~= "" and gname ~= UNKNOWN then
|
||
castName = gname
|
||
SFrames.guidToName[guid] = gname
|
||
end
|
||
-- Fallback: scan known unitIDs with UnitGUID
|
||
if not castName and UnitGUID then
|
||
local scanUnits = {"target","targettarget","party1","party2","party3","party4",
|
||
"party1target","party2target","party3target","party4target"}
|
||
local rn = GetNumRaidMembers and GetNumRaidMembers() or 0
|
||
for ri = 1, rn do
|
||
table.insert(scanUnits, "raid"..ri)
|
||
table.insert(scanUnits, "raid"..ri.."target")
|
||
end
|
||
for _, u in ipairs(scanUnits) do
|
||
local ok1, exists = pcall(UnitExists, u)
|
||
if ok1 and exists then
|
||
local ok2, ug = pcall(UnitGUID, u)
|
||
if ok2 and ug and ug == guid then
|
||
local ok3, uname = pcall(UnitName, u)
|
||
if ok3 and uname then
|
||
castName = uname
|
||
SFrames.guidToName[guid] = uname
|
||
end
|
||
break
|
||
end
|
||
end
|
||
end
|
||
end
|
||
-- Write castByName for name-based consumers (Focus castbar)
|
||
if castName then
|
||
SFrames.castByName[castName] = {
|
||
cast = spell,
|
||
start = GetTime(),
|
||
casttime = arg5,
|
||
icon = icon,
|
||
channel = (arg3 == "CHANNEL") or false,
|
||
}
|
||
end
|
||
|
||
SFrames.superwow_active = true
|
||
elseif arg3 == "FAIL" then
|
||
local guid = arg1
|
||
if castdb[guid] then
|
||
castdb[guid].cast = nil
|
||
castdb[guid].rank = nil
|
||
castdb[guid].start = nil
|
||
castdb[guid].casttime = nil
|
||
castdb[guid].icon = nil
|
||
castdb[guid].channel = nil
|
||
end
|
||
-- Clear castByName
|
||
local failName = SFrames.guidToName[guid]
|
||
if not failName then
|
||
local ok_n, gname = pcall(UnitName, guid)
|
||
if ok_n and gname and gname ~= "" and gname ~= UNKNOWN then failName = gname end
|
||
end
|
||
if failName and SFrames.castByName[failName] then
|
||
SFrames.castByName[failName] = nil
|
||
end
|
||
end
|
||
end)
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Turtle WoW Compatibility
|
||
-- Hides TW's built-in target HP text that overlaps with Nanami-UI frames,
|
||
-- and applies other TW-specific fixes.
|
||
--------------------------------------------------------------------------------
|
||
local function ApplyWorldMapWindowLayout()
|
||
WorldMapFrame:SetMovable(true)
|
||
WorldMapFrame:EnableMouse(true)
|
||
WorldMapFrame:SetScale(.85)
|
||
WorldMapFrame:ClearAllPoints()
|
||
WorldMapFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 30)
|
||
WorldMapFrame:SetWidth(WorldMapButton:GetWidth() + 15)
|
||
WorldMapFrame:SetHeight(WorldMapButton:GetHeight() + 55)
|
||
if WorldMapFrameTitle then
|
||
WorldMapFrameTitle:SetPoint("TOP", WorldMapFrame, 0, 17)
|
||
end
|
||
BlackoutWorld:Hide()
|
||
end
|
||
|
||
local function InitTurtleCompat()
|
||
if not TargetHPText or not TargetHPPercText then return end
|
||
|
||
TargetHPText:Hide()
|
||
TargetHPText.Show = function() return end
|
||
|
||
TargetHPPercText:Hide()
|
||
TargetHPPercText.Show = function() return end
|
||
|
||
if WorldMapFrame_Maximize then
|
||
local origMaximize = WorldMapFrame_Maximize
|
||
WorldMapFrame_Maximize = function()
|
||
origMaximize()
|
||
local cfg = GetTweaksCfg()
|
||
if cfg.worldMapWindow ~= false then
|
||
ApplyWorldMapWindowLayout()
|
||
elseif WorldMapFrameTitle then
|
||
WorldMapFrameTitle:SetPoint("TOP", WorldMapFrame, 0, 17)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- WorldMap Window
|
||
-- Turn the fullscreen world map into a movable, scalable window.
|
||
-- Ctrl+Scroll to zoom, Shift+Scroll to change transparency, drag to move.
|
||
--------------------------------------------------------------------------------
|
||
local function HookScript(frame, script, func)
|
||
local prev = frame:GetScript(script)
|
||
frame:SetScript(script, function(a1,a2,a3,a4,a5,a6,a7,a8,a9)
|
||
if prev then prev(a1,a2,a3,a4,a5,a6,a7,a8,a9) end
|
||
func(a1,a2,a3,a4,a5,a6,a7,a8,a9)
|
||
end)
|
||
end
|
||
|
||
local function InitWorldMapWindow()
|
||
if Cartographer or METAMAP_TITLE then return end
|
||
|
||
table.insert(UISpecialFrames, "WorldMapFrame")
|
||
|
||
local _G = getfenv(0)
|
||
_G.ToggleWorldMap = function()
|
||
if WorldMapFrame:IsShown() then
|
||
WorldMapFrame:Hide()
|
||
else
|
||
WorldMapFrame:Show()
|
||
end
|
||
end
|
||
|
||
UIPanelWindows["WorldMapFrame"] = { area = "center" }
|
||
|
||
HookScript(WorldMapFrame, "OnShow", function()
|
||
this:EnableKeyboard(false)
|
||
this:EnableMouseWheel(1)
|
||
WorldMapFrame:SetScale(.85)
|
||
WorldMapFrame:SetAlpha(1)
|
||
WorldMapFrame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||
end)
|
||
|
||
HookScript(WorldMapFrame, "OnMouseWheel", function()
|
||
if IsShiftKeyDown() then
|
||
local newAlpha = WorldMapFrame:GetAlpha() + arg1 / 10
|
||
if newAlpha < 0.2 then newAlpha = 0.2 end
|
||
if newAlpha > 1 then newAlpha = 1 end
|
||
WorldMapFrame:SetAlpha(newAlpha)
|
||
elseif IsControlKeyDown() then
|
||
local newScale = WorldMapFrame:GetScale() + arg1 / 10
|
||
if newScale < 0.4 then newScale = 0.4 end
|
||
if newScale > 1.5 then newScale = 1.5 end
|
||
WorldMapFrame:SetScale(newScale)
|
||
end
|
||
end)
|
||
|
||
HookScript(WorldMapFrame, "OnMouseDown", function()
|
||
WorldMapFrame:StartMoving()
|
||
end)
|
||
|
||
HookScript(WorldMapFrame, "OnMouseUp", function()
|
||
WorldMapFrame:StopMovingOrSizing()
|
||
end)
|
||
|
||
ApplyWorldMapWindowLayout()
|
||
|
||
-- WorldMapTooltip: raw textures on a child frame (SetBackdrop is unreliable)
|
||
if WorldMapTooltip and not WorldMapTooltip._nanamiBG then
|
||
WorldMapTooltip._nanamiBG = true
|
||
|
||
local wmtBgFrame = CreateFrame("Frame", nil, WorldMapTooltip)
|
||
wmtBgFrame:SetAllPoints(WorldMapTooltip)
|
||
wmtBgFrame:SetFrameLevel(math.max(0, WorldMapTooltip:GetFrameLevel()))
|
||
|
||
local bg = wmtBgFrame:CreateTexture(nil, "BACKGROUND")
|
||
bg:SetTexture("Interface\\Buttons\\WHITE8X8")
|
||
bg:SetVertexColor(0.05, 0.05, 0.05, 1)
|
||
bg:SetAllPoints(wmtBgFrame)
|
||
|
||
local function MakeEdge(p1, r1, p2, r2, w, h)
|
||
local t = wmtBgFrame:CreateTexture(nil, "BORDER")
|
||
t:SetTexture("Interface\\Buttons\\WHITE8X8")
|
||
t:SetVertexColor(0.25, 0.25, 0.25, 1)
|
||
t:SetPoint(p1, WorldMapTooltip, r1)
|
||
t:SetPoint(p2, WorldMapTooltip, r2)
|
||
if w then t:SetWidth(w) end
|
||
if h then t:SetHeight(h) end
|
||
end
|
||
MakeEdge("TOPLEFT","TOPLEFT","TOPRIGHT","TOPRIGHT", nil, 1)
|
||
MakeEdge("BOTTOMLEFT","BOTTOMLEFT","BOTTOMRIGHT","BOTTOMRIGHT", nil, 1)
|
||
MakeEdge("TOPLEFT","TOPLEFT","BOTTOMLEFT","BOTTOMLEFT", 1, nil)
|
||
MakeEdge("TOPRIGHT","TOPRIGHT","BOTTOMRIGHT","BOTTOMRIGHT", 1, nil)
|
||
end
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Cooldown Numbers
|
||
-- Display remaining duration as text on every cooldown frame (>= 2 sec).
|
||
--------------------------------------------------------------------------------
|
||
local function TimeConvert(remaining)
|
||
local color = "|cffffffff"
|
||
if remaining < 5 then
|
||
color = "|cffff5555"
|
||
elseif remaining < 10 then
|
||
color = "|cffffff55"
|
||
end
|
||
|
||
if remaining < 60 then
|
||
return color .. math.ceil(remaining)
|
||
elseif remaining < 3600 then
|
||
return color .. math.ceil(remaining / 60) .. "m"
|
||
elseif remaining < 86400 then
|
||
return color .. math.ceil(remaining / 3600) .. "h"
|
||
else
|
||
return color .. math.ceil(remaining / 86400) .. "d"
|
||
end
|
||
end
|
||
|
||
local _activeCooldowns = {}
|
||
local _cdTickTimer = 0
|
||
local _cdUpdaterFrame
|
||
|
||
local function CooldownSharedUpdate()
|
||
_cdTickTimer = _cdTickTimer + arg1
|
||
if _cdTickTimer < 0.1 then return end
|
||
_cdTickTimer = 0
|
||
|
||
local now = GetTime()
|
||
local sysTime = time()
|
||
|
||
local n = table.getn(_activeCooldowns)
|
||
local i = 1
|
||
while i <= n do
|
||
local cdFrame = _activeCooldowns[i]
|
||
if cdFrame and cdFrame:IsShown() then
|
||
local parent = cdFrame:GetParent()
|
||
if parent then
|
||
cdFrame:SetAlpha(parent:GetAlpha())
|
||
end
|
||
if cdFrame.start < now then
|
||
local remaining = cdFrame.duration - (now - cdFrame.start)
|
||
if remaining > 0 then
|
||
cdFrame.text:SetText(TimeConvert(remaining))
|
||
else
|
||
cdFrame:Hide()
|
||
_activeCooldowns[i] = _activeCooldowns[n]
|
||
_activeCooldowns[n] = nil
|
||
n = n - 1
|
||
i = i - 1
|
||
end
|
||
else
|
||
local startupTime = sysTime - now
|
||
local cdTime = (2 ^ 32) / 1000 - cdFrame.start
|
||
local cdStartTime = startupTime - cdTime
|
||
local cdEndTime = cdStartTime + cdFrame.duration
|
||
local remaining = cdEndTime - sysTime
|
||
if remaining >= 0 then
|
||
cdFrame.text:SetText(TimeConvert(remaining))
|
||
else
|
||
cdFrame:Hide()
|
||
_activeCooldowns[i] = _activeCooldowns[n]
|
||
_activeCooldowns[n] = nil
|
||
n = n - 1
|
||
i = i - 1
|
||
end
|
||
end
|
||
i = i + 1
|
||
else
|
||
_activeCooldowns[i] = _activeCooldowns[n]
|
||
_activeCooldowns[n] = nil
|
||
n = n - 1
|
||
end
|
||
end
|
||
if n == 0 and _cdUpdaterFrame then
|
||
_cdUpdaterFrame:Hide()
|
||
end
|
||
end
|
||
|
||
local function RegisterCooldownFrame(cdFrame)
|
||
for i = 1, table.getn(_activeCooldowns) do
|
||
if _activeCooldowns[i] == cdFrame then return end
|
||
end
|
||
table.insert(_activeCooldowns, cdFrame)
|
||
if not _cdUpdaterFrame then
|
||
_cdUpdaterFrame = CreateFrame("Frame", "NanamiCDSharedUpdater", UIParent)
|
||
_cdUpdaterFrame:SetScript("OnUpdate", CooldownSharedUpdate)
|
||
end
|
||
_cdUpdaterFrame:Show()
|
||
end
|
||
|
||
local function CooldownOnUpdate()
|
||
local parent = this:GetParent()
|
||
if not parent then this:Hide() return end
|
||
|
||
if not this.tick then this.tick = GetTime() + 0.1 end
|
||
if this.tick > GetTime() then return end
|
||
this.tick = GetTime() + 0.1
|
||
|
||
this:SetAlpha(parent:GetAlpha())
|
||
|
||
if this.start < GetTime() then
|
||
local remaining = this.duration - (GetTime() - this.start)
|
||
if remaining > 0 then
|
||
this.text:SetText(TimeConvert(remaining))
|
||
else
|
||
this:Hide()
|
||
end
|
||
else
|
||
local time = time()
|
||
local startupTime = time - GetTime()
|
||
local cdTime = (2 ^ 32) / 1000 - this.start
|
||
local cdStartTime = startupTime - cdTime
|
||
local cdEndTime = cdStartTime + this.duration
|
||
local remaining = cdEndTime - time
|
||
if remaining >= 0 then
|
||
this.text:SetText(TimeConvert(remaining))
|
||
else
|
||
this:Hide()
|
||
end
|
||
end
|
||
end
|
||
|
||
local function IsActionBarButtonName(name)
|
||
if not name then return false end
|
||
return string.find(name, "^ActionButton%d+$")
|
||
or string.find(name, "^BonusActionButton%d+$")
|
||
or string.find(name, "^MultiBarBottomLeftButton%d+$")
|
||
or string.find(name, "^MultiBarBottomRightButton%d+$")
|
||
or string.find(name, "^MultiBarLeftButton%d+$")
|
||
or string.find(name, "^MultiBarRightButton%d+$")
|
||
or string.find(name, "^PetActionButton%d+$")
|
||
or string.find(name, "^ShapeshiftButton%d+$")
|
||
end
|
||
|
||
local function UpdateActionBarCooldownMask(cooldown)
|
||
if not cooldown then return end
|
||
if cooldown.cooldownmask then
|
||
cooldown.cooldownmask:SetAllPoints(cooldown)
|
||
cooldown.cooldownmask:SetFrameStrata(cooldown:GetFrameStrata())
|
||
cooldown.cooldownmask:SetFrameLevel(cooldown:GetFrameLevel() + 1)
|
||
end
|
||
if cooldown.cooldowntext then
|
||
cooldown.cooldowntext:SetAllPoints(cooldown)
|
||
cooldown.cooldowntext:SetFrameStrata(cooldown:GetFrameStrata())
|
||
cooldown.cooldowntext:SetFrameLevel(cooldown:GetFrameLevel() + 2)
|
||
end
|
||
end
|
||
|
||
local function CreateCoolDown(cooldown, start, duration)
|
||
if not cooldown then return end
|
||
local parent = cooldown:GetParent()
|
||
if not parent then return end
|
||
if cooldown.readable then return end
|
||
|
||
local parentname = parent and parent.GetName and parent:GetName()
|
||
parentname = parentname or "UnknownCooldownFrame"
|
||
|
||
cooldown.cooldowntext = CreateFrame("Frame", parentname .. "NanamiCDText", cooldown)
|
||
cooldown.cooldowntext:SetAllPoints(cooldown)
|
||
cooldown.cooldowntext:SetFrameStrata(cooldown:GetFrameStrata())
|
||
cooldown.cooldowntext:SetFrameLevel(cooldown:GetFrameLevel() + 2)
|
||
cooldown.cooldowntext.text = cooldown.cooldowntext:CreateFontString(
|
||
parentname .. "NanamiCDFont", "OVERLAY")
|
||
|
||
local isActionBar = IsActionBarButtonName(parentname)
|
||
local size = parent:GetHeight() or 0
|
||
size = size > 0 and size * 0.64 or 12
|
||
size = size > 14 and 14 or size
|
||
|
||
if isActionBar then
|
||
local bigSize = size * 1.22
|
||
if bigSize < 13 then bigSize = 13 end
|
||
if bigSize > 18 then bigSize = 18 end
|
||
cooldown.cooldowntext.text:SetFont(STANDARD_TEXT_FONT, bigSize, "THICKOUTLINE")
|
||
else
|
||
cooldown.cooldowntext.text:SetFont(STANDARD_TEXT_FONT, size, "OUTLINE")
|
||
end
|
||
cooldown.cooldowntext.text:SetDrawLayer("OVERLAY", 7)
|
||
|
||
if isActionBar then
|
||
cooldown.cooldownmask = CreateFrame("Frame", parentname .. "NanamiCDMask", cooldown)
|
||
cooldown.cooldownmask:SetAllPoints(cooldown)
|
||
cooldown.cooldownmask:SetFrameStrata(cooldown:GetFrameStrata())
|
||
cooldown.cooldownmask:SetFrameLevel(cooldown:GetFrameLevel() + 1)
|
||
local mask = cooldown.cooldownmask:CreateTexture(nil, "BACKGROUND")
|
||
mask:SetTexture("Interface\\Buttons\\WHITE8X8")
|
||
mask:SetAllPoints(cooldown.cooldownmask)
|
||
mask:SetDrawLayer("BACKGROUND", 0)
|
||
mask:SetVertexColor(0, 0, 0, 0.45)
|
||
cooldown.cooldownmask.mask = mask
|
||
cooldown.cooldowntext.text:SetPoint("CENTER", cooldown.cooldowntext, "CENTER", 0, 0)
|
||
else
|
||
cooldown.cooldowntext.text:SetPoint("CENTER", cooldown.cooldowntext, "CENTER", 0, 0)
|
||
end
|
||
|
||
RegisterCooldownFrame(cooldown.cooldowntext)
|
||
end
|
||
|
||
local function SetCooldown(frame, start, duration, enable)
|
||
if not frame then return end
|
||
if frame.noCooldownCount then return end
|
||
|
||
if not duration or duration < 2 then
|
||
if frame.cooldowntext then
|
||
frame.cooldowntext:Hide()
|
||
end
|
||
if frame.cooldownmask then
|
||
frame.cooldownmask:Hide()
|
||
end
|
||
return
|
||
end
|
||
|
||
if not frame.cooldowntext then
|
||
CreateCoolDown(frame, start, duration)
|
||
end
|
||
|
||
if frame.cooldowntext then
|
||
UpdateActionBarCooldownMask(frame)
|
||
if start > 0 and duration > 0 and (not enable or enable > 0) then
|
||
if frame.cooldownmask then frame.cooldownmask:Show() end
|
||
frame.cooldowntext:Show()
|
||
frame.cooldowntext.start = start
|
||
frame.cooldowntext.duration = duration
|
||
RegisterCooldownFrame(frame.cooldowntext)
|
||
else
|
||
if frame.cooldownmask then frame.cooldownmask:Hide() end
|
||
frame.cooldowntext:Hide()
|
||
end
|
||
end
|
||
end
|
||
|
||
local function InitCooldownNumbers()
|
||
_hooksecurefunc("CooldownFrame_SetTimer", SetCooldown)
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Quest Watch Timed Quest Countdown
|
||
-- Append remaining time for timed quests in the watched quest list (outside
|
||
-- quest detail UI), so players can see countdown directly on the main HUD.
|
||
--------------------------------------------------------------------------------
|
||
local function FormatQuestCountdown(seconds)
|
||
local s = tonumber(seconds) or 0
|
||
if s <= 0 then return "0s" end
|
||
|
||
local h = math.floor(s / 3600)
|
||
local m = math.floor(math.mod(s, 3600) / 60)
|
||
local sec = math.floor(math.mod(s, 60))
|
||
|
||
if h > 0 then
|
||
return string.format("%dh %02dm %02ds", h, m, sec)
|
||
elseif m > 0 then
|
||
return string.format("%dm %02ds", m, sec)
|
||
end
|
||
return string.format("%ds", sec)
|
||
end
|
||
|
||
local function InitQuestWatchCountdown()
|
||
if not QuestWatch_Update or not GetNumQuestWatches or not GetQuestLogTitle
|
||
or not GetQuestIndexForWatch or not GetQuestLogTimeLeft then
|
||
return
|
||
end
|
||
|
||
local function StripNanamiCountdown(text)
|
||
if not text then return text end
|
||
return string.gsub(text, " |cffffcc00%[剩余: [^%]]+%]|r$", "")
|
||
end
|
||
|
||
local applying = false
|
||
local function ApplyQuestWatchCountdown()
|
||
if applying then return end
|
||
applying = true
|
||
|
||
local timedByTitle = {}
|
||
local watchCount = tonumber(GetNumQuestWatches()) or 0
|
||
for i = 1, watchCount do
|
||
local questIndex = GetQuestIndexForWatch(i)
|
||
if questIndex and questIndex > 0 then
|
||
local title, level, tag, isHeader = GetQuestLogTitle(questIndex)
|
||
if title and not isHeader then
|
||
local timeLeft = GetQuestLogTimeLeft(questIndex)
|
||
if timeLeft and timeLeft > 0 then
|
||
timedByTitle[title] = FormatQuestCountdown(timeLeft)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
local lineCount = tonumber(QUESTWATCHLINES) or 0
|
||
for i = 1, lineCount do
|
||
local fs = _G["QuestWatchLine" .. i]
|
||
if fs and fs.GetText and fs.SetText then
|
||
local text = fs:GetText()
|
||
if text and text ~= "" then
|
||
local clean = StripNanamiCountdown(text)
|
||
local timerText = timedByTitle[clean]
|
||
if timerText then
|
||
fs:SetText(clean .. " |cffffcc00[剩余: " .. timerText .. "]|r")
|
||
elseif clean ~= text then
|
||
fs:SetText(clean)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
applying = false
|
||
end
|
||
|
||
_hooksecurefunc("QuestWatch_Update", ApplyQuestWatchCountdown)
|
||
|
||
local ticker = CreateFrame("Frame", "NanamiQuestWatchCountdownTicker")
|
||
ticker._e = 0
|
||
ticker:SetScript("OnUpdate", function()
|
||
this._e = (this._e or 0) + (arg1 or 0)
|
||
if this._e < 1.0 then return end
|
||
this._e = 0
|
||
if (tonumber(GetNumQuestWatches()) or 0) > 0 then
|
||
pcall(ApplyQuestWatchCountdown)
|
||
end
|
||
end)
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Dark UI
|
||
-- Turns the entire interface into darker colors by applying vertex color
|
||
-- tinting to all UI textures recursively.
|
||
--------------------------------------------------------------------------------
|
||
local function HookAddonOrVariable(addon, func)
|
||
local lurker = CreateFrame("Frame", nil)
|
||
lurker.func = func
|
||
lurker:RegisterEvent("ADDON_LOADED")
|
||
lurker:RegisterEvent("VARIABLES_LOADED")
|
||
lurker:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||
lurker:SetScript("OnEvent", function()
|
||
if IsAddOnLoaded(addon) or getfenv(0)[addon] then
|
||
this:func()
|
||
this:UnregisterAllEvents()
|
||
end
|
||
end)
|
||
end
|
||
|
||
local borderBackdrop = {
|
||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||
tile = true, tileSize = 8, edgeSize = 16,
|
||
insets = { left = 0, right = 0, top = 0, bottom = 0 }
|
||
}
|
||
|
||
local function AddBorder(frame, inset, color)
|
||
if not frame then return end
|
||
if frame.NanamiBorder then return frame.NanamiBorder end
|
||
|
||
local top, right, bottom, left
|
||
if type(inset) == "table" then
|
||
top, right, bottom, left = unpack(inset)
|
||
left, bottom = -left, -bottom
|
||
end
|
||
|
||
frame.NanamiBorder = CreateFrame("Frame", nil, frame)
|
||
frame.NanamiBorder:SetPoint("TOPLEFT", frame, "TOPLEFT",
|
||
(left or -inset), (top or inset))
|
||
frame.NanamiBorder:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT",
|
||
(right or inset), (bottom or -inset))
|
||
frame.NanamiBorder:SetBackdrop(borderBackdrop)
|
||
if color then
|
||
frame.NanamiBorder:SetBackdropBorderColor(color.r, color.g, color.b, 1)
|
||
end
|
||
return frame.NanamiBorder
|
||
end
|
||
|
||
local darkColor = { r = .3, g = .3, b = .3, a = .9 }
|
||
|
||
local darkBlacklist = {
|
||
["Solid Texture"] = true, ["WHITE8X8"] = true, ["StatusBar"] = true,
|
||
["BarFill"] = true, ["Portrait"] = true, ["Button"] = true,
|
||
["Icon"] = true, ["AddOns"] = true, ["StationeryTest"] = true,
|
||
["TargetDead"] = true, ["^KeyRing"] = true, ["GossipIcon"] = true,
|
||
["WorldMap\\(.+)\\"] = true, ["PetHappiness"] = true,
|
||
["Elite"] = true, ["Rare"] = true, ["ColorPickerWheel"] = true,
|
||
["ComboPoint"] = true, ["Skull"] = true,
|
||
["battlenetworking0"] = true, ["damage"] = true,
|
||
["tank"] = true, ["healer"] = true,
|
||
}
|
||
|
||
local darkRegionSkips = {
|
||
["ColorPickerFrame"] = { [15] = true }
|
||
}
|
||
|
||
local darkBackgrounds = {
|
||
["^SpellBookFrame$"] = { 325, 355, 17, -74 },
|
||
["^ItemTextFrame$"] = { 300, 355, 24, -74 },
|
||
}
|
||
|
||
local darkBorders = {
|
||
["ShapeshiftButton"] = 3, ["BuffButton"] = 3,
|
||
["TargetFrameBuff"] = 3, ["TempEnchant"] = 3,
|
||
["SpellButton"] = 3, ["SpellBookSkillLineTab"] = 3,
|
||
["ActionButton%d+$"] = 3, ["MultiBar(.+)Button%d+$"] = 3,
|
||
["KeyRingButton"] = 2,
|
||
["ActionBarUpButton"] = -3, ["ActionBarDownButton"] = -3,
|
||
["Character(.+)Slot$"] = 3, ["Inspect(.+)Slot$"] = 3,
|
||
["ContainerFrame(.+)Item"] = 3, ["MainMenuBarBackpackButton$"] = 3,
|
||
["CharacterBag(.+)Slot$"] = 3, ["ChatFrame(.+)Button"] = -2,
|
||
["PetFrameHappiness"] = 2, ["MicroButton"] = { -21, 0, 0, 0 },
|
||
}
|
||
|
||
local darkAddonFrames = {
|
||
["Blizzard_TalentUI"] = { "TalentFrame" },
|
||
["Blizzard_AuctionUI"] = { "AuctionFrame", "AuctionDressUpFrame" },
|
||
["Blizzard_CraftUI"] = { "CraftFrame" },
|
||
["Blizzard_InspectUI"] = { "InspectPaperDollFrame", "InspectHonorFrame", "InspectFrameTab1", "InspectFrameTab2" },
|
||
["Blizzard_MacroUI"] = { "MacroFrame", "MacroPopupFrame" },
|
||
["Blizzard_RaidUI"] = { "ReadyCheckFrame" },
|
||
["Blizzard_TradeSkillUI"] = { "TradeSkillFrame" },
|
||
-- ClassTrainerFrame replaced by TrainerUI.lua
|
||
}
|
||
|
||
local function IsDarkBlacklisted(texture)
|
||
local name = texture:GetName()
|
||
local tex = texture:GetTexture()
|
||
if not tex then return true end
|
||
|
||
if name then
|
||
for entry in pairs(darkBlacklist) do
|
||
if string.find(name, entry, 1) then return true end
|
||
end
|
||
end
|
||
|
||
for entry in pairs(darkBlacklist) do
|
||
if string.find(tex, entry, 1) then return true end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
local function AddSpecialBackground(frame, w, h, x, y)
|
||
frame.NanamiMaterial = frame.NanamiMaterial or frame:CreateTexture(nil, "OVERLAY")
|
||
frame.NanamiMaterial:SetTexture("Interface\\Stationery\\StationeryTest1")
|
||
frame.NanamiMaterial:SetWidth(w)
|
||
frame.NanamiMaterial:SetHeight(h)
|
||
frame.NanamiMaterial:SetPoint("TOPLEFT", frame, x, y)
|
||
frame.NanamiMaterial:SetVertexColor(.8, .8, .8)
|
||
end
|
||
|
||
local darkFrameSkips = {
|
||
["^SFramesChat"] = true,
|
||
["^SFramesPlayer"] = true,
|
||
["^SFramesTarget"] = true,
|
||
["^SFramesParty"] = true,
|
||
["^SFramesRaid"] = true,
|
||
["^SFramesMBuff"] = true,
|
||
["^SFramesMDebuff"] = true,
|
||
["^GameTooltip"] = true,
|
||
}
|
||
|
||
local function DarkenFrame(frame, r, g, b, a)
|
||
if not r and not g and not b then
|
||
r, g, b, a = darkColor.r, darkColor.g, darkColor.b, darkColor.a
|
||
end
|
||
|
||
local fname = frame and frame.GetName and frame:GetName()
|
||
if fname then
|
||
for pattern in pairs(darkFrameSkips) do
|
||
if string.find(fname, pattern) then return end
|
||
end
|
||
end
|
||
|
||
if frame and frame.GetChildren then
|
||
for _, child in pairs({ frame:GetChildren() }) do
|
||
DarkenFrame(child, r, g, b, a)
|
||
end
|
||
end
|
||
|
||
if frame and frame.GetRegions then
|
||
local name = frame.GetName and frame:GetName()
|
||
|
||
if frame.SetBackdropBorderColor then
|
||
frame:SetBackdropBorderColor(darkColor.r, darkColor.g, darkColor.b, darkColor.a)
|
||
end
|
||
|
||
for pattern, inset in pairs(darkBackgrounds) do
|
||
if name and string.find(name, pattern) then
|
||
AddSpecialBackground(frame, inset[1], inset[2], inset[3], inset[4])
|
||
end
|
||
end
|
||
|
||
for pattern, inset in pairs(darkBorders) do
|
||
if name and string.find(name, pattern) then
|
||
AddBorder(frame, inset, darkColor)
|
||
end
|
||
end
|
||
|
||
for id, region in pairs({ frame:GetRegions() }) do
|
||
if region.SetVertexColor and region:GetObjectType() == "Texture" then
|
||
if name and id and darkRegionSkips[name] and darkRegionSkips[name][id] then
|
||
-- skip
|
||
elseif region.GetBlendMode and region:GetBlendMode() == "ADD" then
|
||
-- skip blend textures
|
||
elseif IsDarkBlacklisted(region) then
|
||
-- skip blacklisted
|
||
else
|
||
region:SetVertexColor(r, g, b, a)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- StaticPopup Theme Skin
|
||
--------------------------------------------------------------------------------
|
||
local function InitPopupSkin()
|
||
local popupSkinned = {}
|
||
local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARKai_T.ttf"
|
||
|
||
local _A = SFrames.ActiveTheme
|
||
local P = {
|
||
bg = _A.panelBg or { 0.12, 0.06, 0.10, 0.95 },
|
||
border = _A.panelBorder or { 0.55, 0.30, 0.42, 0.9 },
|
||
btnBg = _A.btnBg or { 0.18, 0.10, 0.15, 0.94 },
|
||
btnBd = _A.btnBorder or { 0.50, 0.30, 0.40, 0.80 },
|
||
text = _A.nameText or { 0.90, 0.88, 0.94 },
|
||
}
|
||
|
||
local function HidePopupTex(tex)
|
||
if not tex then return end
|
||
if tex.SetTexture then tex:SetTexture(nil) end
|
||
if tex.SetAlpha then tex:SetAlpha(0) end
|
||
if tex.Hide then tex:Hide() end
|
||
end
|
||
|
||
local function SkinButton(btn)
|
||
if not btn then return end
|
||
local regions = { btn:GetRegions() }
|
||
for _, r in ipairs(regions) do
|
||
if r and r.SetTexture and r:GetObjectType() == "Texture" then
|
||
local tex = r:GetTexture()
|
||
if tex and type(tex) == "string" and (string.find(tex, "UI%-Panel") or string.find(tex, "UI%-DialogBox")) then
|
||
r:Hide()
|
||
end
|
||
end
|
||
end
|
||
|
||
HidePopupTex(btn.GetNormalTexture and btn:GetNormalTexture())
|
||
HidePopupTex(btn.GetPushedTexture and btn:GetPushedTexture())
|
||
HidePopupTex(btn.GetHighlightTexture and btn:GetHighlightTexture())
|
||
HidePopupTex(btn.GetDisabledTexture and btn:GetDisabledTexture())
|
||
|
||
local btnName = btn:GetName() or ""
|
||
for _, sfx in ipairs({"Left", "Right", "Middle"}) do
|
||
local t = _G[btnName .. sfx]
|
||
if t then t:SetAlpha(0); t:Hide() end
|
||
end
|
||
|
||
btn:SetBackdrop({
|
||
bgFile = "Interface\\Buttons\\WHITE8X8",
|
||
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
||
tile = false, tileSize = 0, edgeSize = 1,
|
||
insets = { left = 1, right = 1, top = 1, bottom = 1 },
|
||
})
|
||
btn:SetBackdropColor(P.btnBg[1], P.btnBg[2], P.btnBg[3], P.btnBg[4])
|
||
btn:SetBackdropBorderColor(P.btnBd[1], P.btnBd[2], P.btnBd[3], P.btnBd[4])
|
||
|
||
local fs = btn:GetFontString()
|
||
if fs then
|
||
fs:SetFont(font, 12, "OUTLINE")
|
||
fs:SetTextColor(P.text[1], P.text[2], P.text[3])
|
||
end
|
||
|
||
if not btn.nanamiPopupStyled then
|
||
btn.nanamiPopupStyled = true
|
||
local origEnter = btn:GetScript("OnEnter")
|
||
local origLeave = btn:GetScript("OnLeave")
|
||
|
||
btn:SetScript("OnEnter", function()
|
||
if origEnter then origEnter() end
|
||
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
|
||
if _A.btnHoverBorder then
|
||
this:SetBackdropBorderColor(_A.btnHoverBorder[1], _A.btnHoverBorder[2], _A.btnHoverBorder[3], _A.btnHoverBorder[4])
|
||
elseif _A.btnHoverBd then
|
||
this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4])
|
||
end
|
||
local t = this:GetFontString()
|
||
if t and _A.btnActiveText then t:SetTextColor(_A.btnActiveText[1], _A.btnActiveText[2], _A.btnActiveText[3]) end
|
||
end)
|
||
btn:SetScript("OnLeave", function()
|
||
if origLeave then origLeave() end
|
||
this:SetBackdropColor(P.btnBg[1], P.btnBg[2], P.btnBg[3], P.btnBg[4])
|
||
this:SetBackdropBorderColor(P.btnBd[1], P.btnBd[2], P.btnBd[3], P.btnBd[4])
|
||
local t = this:GetFontString()
|
||
if t then t:SetTextColor(P.text[1], P.text[2], P.text[3]) end
|
||
end)
|
||
btn:SetScript("OnMouseDown", function()
|
||
if _A.btnDownBg then
|
||
this:SetBackdropColor(_A.btnDownBg[1], _A.btnDownBg[2], _A.btnDownBg[3], _A.btnDownBg[4])
|
||
end
|
||
end)
|
||
btn:SetScript("OnMouseUp", function()
|
||
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
|
||
end)
|
||
end
|
||
end
|
||
|
||
local function SkinPopupFrame(frame)
|
||
if not frame then return end
|
||
local frameName = frame:GetName()
|
||
if not frameName then return end
|
||
|
||
if not popupSkinned[frameName] then
|
||
popupSkinned[frameName] = true
|
||
|
||
local regions = { frame:GetRegions() }
|
||
for _, r in ipairs(regions) do
|
||
if r and r:GetObjectType() == "Texture" then
|
||
local dl = r:GetDrawLayer()
|
||
if dl == "BACKGROUND" or dl == "BORDER" or dl == "ARTWORK" then
|
||
local tex = r:GetTexture() or ""
|
||
if type(tex) == "string" and (string.find(tex, "UI%-DialogBox") or string.find(tex, "UI%-Panel")) then
|
||
r:Hide()
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
frame:SetBackdrop({
|
||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||
tile = true, tileSize = 16, edgeSize = 14,
|
||
insets = { left = 3, right = 3, top = 3, bottom = 3 },
|
||
})
|
||
frame:SetBackdropColor(P.bg[1], P.bg[2], P.bg[3], P.bg[4])
|
||
frame:SetBackdropBorderColor(P.border[1], P.border[2], P.border[3], P.border[4])
|
||
|
||
local textFS = _G[frameName .. "Text"]
|
||
if textFS and textFS.SetFont then
|
||
textFS:SetFont(font, 13, "OUTLINE")
|
||
textFS:SetTextColor(P.text[1], P.text[2], P.text[3])
|
||
end
|
||
|
||
local editBox = _G[frameName .. "EditBox"]
|
||
if editBox then
|
||
editBox:SetBackdrop({
|
||
bgFile = "Interface\\Buttons\\WHITE8X8",
|
||
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
||
tile = false, edgeSize = 1,
|
||
insets = { left = 1, right = 1, top = 1, bottom = 1 },
|
||
})
|
||
editBox:SetBackdropColor(0.05, 0.03, 0.05, 0.9)
|
||
editBox:SetBackdropBorderColor(P.btnBd[1], P.btnBd[2], P.btnBd[3], P.btnBd[4])
|
||
editBox:SetFont(font, 12, "OUTLINE")
|
||
editBox:SetTextColor(P.text[1], P.text[2], P.text[3])
|
||
end
|
||
|
||
local moneyFrame = _G[frameName .. "MoneyFrame"]
|
||
if moneyFrame then
|
||
local mRegions = { moneyFrame:GetRegions() }
|
||
for _, r in ipairs(mRegions) do
|
||
if r and r.SetFont then r:SetFont(font, 12, "OUTLINE") end
|
||
end
|
||
end
|
||
end
|
||
|
||
for _, suffix in ipairs({"Button1", "Button2", "Button3"}) do
|
||
local btn = _G[frameName .. suffix]
|
||
if btn then SkinButton(btn) end
|
||
end
|
||
end
|
||
|
||
for i = 1, 4 do
|
||
local f = _G["StaticPopup" .. i]
|
||
if f then pcall(SkinPopupFrame, f) end
|
||
end
|
||
|
||
if StaticPopup_Show then
|
||
local origShow = StaticPopup_Show
|
||
StaticPopup_Show = function(a1, a2, a3, a4)
|
||
local dialog = origShow(a1, a2, a3, a4)
|
||
if dialog then
|
||
pcall(SkinPopupFrame, dialog)
|
||
end
|
||
return dialog
|
||
end
|
||
end
|
||
end
|
||
|
||
local function InitDarkUI()
|
||
local hookBuffButton_Update = BuffButton_Update
|
||
BuffButton_Update = function(buttonName, index, filter)
|
||
hookBuffButton_Update(buttonName, index, filter)
|
||
local name = buttonName and index and buttonName .. index or this:GetName()
|
||
local original = getfenv(0)[name .. "Border"]
|
||
if original and this.NanamiBorder then
|
||
local r, g, b = original:GetVertexColor()
|
||
this.NanamiBorder:SetBackdropBorderColor(r, g, b, 1)
|
||
original:SetAlpha(0)
|
||
end
|
||
end
|
||
|
||
TOOLTIP_DEFAULT_COLOR.r = darkColor.r
|
||
TOOLTIP_DEFAULT_COLOR.g = darkColor.g
|
||
TOOLTIP_DEFAULT_COLOR.b = darkColor.b
|
||
|
||
TOOLTIP_DEFAULT_BACKGROUND_COLOR.r = darkColor.r
|
||
TOOLTIP_DEFAULT_BACKGROUND_COLOR.g = darkColor.g
|
||
TOOLTIP_DEFAULT_BACKGROUND_COLOR.b = darkColor.b
|
||
|
||
DarkenFrame(UIParent)
|
||
DarkenFrame(WorldMapFrame)
|
||
DarkenFrame(DropDownList1)
|
||
DarkenFrame(DropDownList2)
|
||
DarkenFrame(DropDownList3)
|
||
|
||
local bars = { "Action", "BonusAction", "MultiBarBottomLeft",
|
||
"MultiBarBottomRight", "MultiBarLeft", "MultiBarRight", "Shapeshift" }
|
||
for _, prefix in pairs(bars) do
|
||
for i = 1, NUM_ACTIONBAR_BUTTONS do
|
||
local button = getfenv(0)[prefix .. "Button" .. i]
|
||
local texture = getfenv(0)[prefix .. "Button" .. i .. "NormalTexture"]
|
||
if button and texture then
|
||
texture:SetWidth(60)
|
||
texture:SetHeight(60)
|
||
texture:SetPoint("CENTER", 0, 0)
|
||
AddBorder(button, 3)
|
||
end
|
||
end
|
||
end
|
||
|
||
for _, button in pairs({ MinimapZoomOut, MinimapZoomIn }) do
|
||
for _, func in pairs({ "GetNormalTexture", "GetDisabledTexture", "GetPushedTexture" }) do
|
||
if button[func] then
|
||
local tex = button[func](button)
|
||
if tex then
|
||
tex:SetVertexColor(darkColor.r + .2, darkColor.g + .2, darkColor.b + .2, 1)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
for addon, data in pairs(darkAddonFrames) do
|
||
local skip = false
|
||
if SFramesDB and SFramesDB.enableTradeSkill ~= false then
|
||
if addon == "Blizzard_TradeSkillUI" or addon == "Blizzard_CraftUI" then
|
||
skip = true
|
||
end
|
||
end
|
||
if not skip then
|
||
for _, frameName in pairs(data) do
|
||
local fn = frameName
|
||
HookAddonOrVariable(fn, function()
|
||
DarkenFrame(getfenv(0)[fn])
|
||
end)
|
||
end
|
||
end
|
||
end
|
||
|
||
HookAddonOrVariable("Blizzard_TimeManager", function()
|
||
DarkenFrame(TimeManagerClockButton)
|
||
end)
|
||
|
||
HookAddonOrVariable("GameTooltipStatusBarBackdrop", function()
|
||
DarkenFrame(getfenv(0)["GameTooltipStatusBarBackdrop"])
|
||
end)
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Hunter Aspect Guard
|
||
-- When a Hunter takes damage in combat with Aspect of the Cheetah or Pack
|
||
-- active, cancel the aspect to reduce daze chains. OOC HP changes are ignored.
|
||
--------------------------------------------------------------------------------
|
||
local function InitHunterAspectGuard()
|
||
local _, playerClass = UnitClass("player")
|
||
if playerClass ~= "HUNTER" then return end
|
||
|
||
local CHEETAH_TEX = "ability_mount_jungletiger"
|
||
local PACK_TEX = "ability_mount_packhorse"
|
||
|
||
local function CancelDangerousAspect()
|
||
for i = 0, 31 do
|
||
local buffIdx = GetPlayerBuff(i, "HELPFUL")
|
||
if buffIdx and buffIdx >= 0 then
|
||
local tex = GetPlayerBuffTexture(buffIdx)
|
||
if tex then
|
||
local lower = string.lower(tex)
|
||
if string.find(lower, CHEETAH_TEX) or string.find(lower, PACK_TEX) then
|
||
CancelPlayerBuff(buffIdx)
|
||
SFrames:Print("受到伤害,已自动取消守护")
|
||
return true
|
||
end
|
||
end
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
local lastHP = UnitHealth("player") or 0
|
||
local lastCancel = 0
|
||
local elapsed = 0
|
||
local frame = CreateFrame("Frame", "NanamiHunterAspectGuard")
|
||
frame:SetScript("OnUpdate", function()
|
||
elapsed = elapsed + (arg1 or 0)
|
||
if elapsed < 0.1 then return end
|
||
elapsed = 0
|
||
|
||
local hp = UnitHealth("player")
|
||
if hp <= 0 then
|
||
lastHP = 0
|
||
return
|
||
end
|
||
|
||
if lastHP > 0 and hp < lastHP and UnitAffectingCombat("player") then
|
||
if GetTime() - lastCancel >= 1.0 then
|
||
if CancelDangerousAspect() then
|
||
lastCancel = GetTime()
|
||
end
|
||
end
|
||
end
|
||
|
||
lastHP = hp
|
||
end)
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Combat Background Notify
|
||
-- Flash the Windows taskbar icon when entering combat (UnitXP SP3 required).
|
||
--------------------------------------------------------------------------------
|
||
local function InitCombatNotify()
|
||
if not (type(UnitXP) == "function" and pcall(UnitXP, "nop", "nop")) then return end
|
||
local f = CreateFrame("Frame", "NanamiCombatNotify")
|
||
f:RegisterEvent("PLAYER_REGEN_DISABLED")
|
||
f:SetScript("OnEvent", function()
|
||
pcall(UnitXP, "notify", "taskbarIcon")
|
||
end)
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Mouseover Cast
|
||
-- When enabled, action bar presses and CastSpellByName calls will target the
|
||
-- unit under the mouse cursor without changing current target.
|
||
--
|
||
-- Strategy:
|
||
-- UseAction hook: temporarily TargetUnit(moUnit) before the real UseAction,
|
||
-- then restore previous target afterwards. This preserves the hardware
|
||
-- event callstack so the client doesn't reject the action.
|
||
-- CastSpellByName hook (SuperWoW): pass moUnit as 2nd arg directly.
|
||
-- CastSpellByName hook (no SuperWoW): same target-swap trick.
|
||
--------------------------------------------------------------------------------
|
||
local mouseoverCastEnabled = false
|
||
local origUseAction = nil
|
||
local origCastSpellByName = nil
|
||
local inMouseoverAction = false -- re-entrancy guard
|
||
|
||
local function GetMouseoverUnit()
|
||
local focus = GetMouseFocus and GetMouseFocus()
|
||
if focus then
|
||
if focus.unit and UnitExists(focus.unit) then
|
||
return focus.unit
|
||
end
|
||
local parent = focus:GetParent()
|
||
if parent and parent.unit and UnitExists(parent.unit) then
|
||
return parent.unit
|
||
end
|
||
end
|
||
if UnitExists("mouseover") then
|
||
return "mouseover"
|
||
end
|
||
return nil
|
||
end
|
||
|
||
local function MouseoverUseAction(action, cursor, onSelf)
|
||
-- Don't interfere: picking up action, or re-entrant call
|
||
if cursor == 1 or inMouseoverAction then
|
||
return origUseAction(action, cursor, onSelf)
|
||
end
|
||
|
||
local moUnit = GetMouseoverUnit()
|
||
if not moUnit then
|
||
return origUseAction(action, cursor, onSelf)
|
||
end
|
||
|
||
-- Skip if mouseover IS current target (no swap needed)
|
||
if UnitIsUnit and UnitExists("target") and UnitIsUnit(moUnit, "target") then
|
||
return origUseAction(action, cursor, onSelf)
|
||
end
|
||
|
||
-- Remember current target state
|
||
local hadTarget = UnitExists("target")
|
||
local prevTargetName = hadTarget and UnitName("target") or nil
|
||
|
||
-- Temporarily target the mouseover unit
|
||
inMouseoverAction = true
|
||
TargetUnit(moUnit)
|
||
|
||
-- Execute the real UseAction on the now-targeted mouseover unit
|
||
origUseAction(action, cursor, onSelf)
|
||
|
||
-- Handle ground-targeted spells (Blizzard, Flamestrike, etc.)
|
||
if SpellIsTargeting and SpellIsTargeting() then
|
||
SpellTargetUnit(moUnit)
|
||
end
|
||
if SpellIsTargeting and SpellIsTargeting() then
|
||
SpellStopTargeting()
|
||
end
|
||
|
||
-- Restore previous target
|
||
if hadTarget and prevTargetName then
|
||
-- Target back the previous unit
|
||
TargetLastTarget()
|
||
-- Verify restoration worked
|
||
if not UnitExists("target") or UnitName("target") ~= prevTargetName then
|
||
-- TargetLastTarget failed, try by name
|
||
TargetByName(prevTargetName, true)
|
||
end
|
||
else
|
||
-- Had no target before, clear
|
||
ClearTarget()
|
||
end
|
||
|
||
inMouseoverAction = false
|
||
end
|
||
|
||
local function MouseoverCastSpellByName(spell, arg2)
|
||
-- Already has explicit target arg, pass through
|
||
if arg2 then
|
||
return origCastSpellByName(spell, arg2)
|
||
end
|
||
|
||
-- Re-entrancy guard (e.g. called from within MouseoverUseAction's chain)
|
||
if inMouseoverAction then
|
||
return origCastSpellByName(spell)
|
||
end
|
||
|
||
local moUnit = GetMouseoverUnit()
|
||
if not moUnit then
|
||
return origCastSpellByName(spell)
|
||
end
|
||
|
||
-- SuperWoW: direct unit parameter, no target swap needed
|
||
if SUPERWOW_VERSION then
|
||
origCastSpellByName(spell, moUnit)
|
||
if SpellIsTargeting and SpellIsTargeting() then
|
||
SpellTargetUnit(moUnit)
|
||
end
|
||
if SpellIsTargeting and SpellIsTargeting() then
|
||
SpellStopTargeting()
|
||
end
|
||
return
|
||
end
|
||
|
||
-- No SuperWoW: target-swap
|
||
local hadTarget = UnitExists("target")
|
||
local prevTargetName = hadTarget and UnitName("target") or nil
|
||
|
||
if not (hadTarget and UnitIsUnit and UnitIsUnit(moUnit, "target")) then
|
||
TargetUnit(moUnit)
|
||
end
|
||
|
||
origCastSpellByName(spell)
|
||
|
||
if SpellIsTargeting and SpellIsTargeting() then
|
||
SpellTargetUnit("target")
|
||
end
|
||
if SpellIsTargeting and SpellIsTargeting() then
|
||
SpellStopTargeting()
|
||
end
|
||
|
||
if hadTarget and prevTargetName then
|
||
TargetLastTarget()
|
||
if not UnitExists("target") or UnitName("target") ~= prevTargetName then
|
||
TargetByName(prevTargetName, true)
|
||
end
|
||
elseif not hadTarget then
|
||
ClearTarget()
|
||
end
|
||
end
|
||
|
||
local function InitMouseoverCast()
|
||
if UseAction then
|
||
origUseAction = UseAction
|
||
UseAction = MouseoverUseAction
|
||
end
|
||
if CastSpellByName then
|
||
origCastSpellByName = CastSpellByName
|
||
CastSpellByName = MouseoverCastSpellByName
|
||
end
|
||
mouseoverCastEnabled = true
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami] 鼠标指向施法已启用" ..
|
||
(SUPERWOW_VERSION and "(SuperWoW 模式)" or "(目标切换模式)") .. "|r")
|
||
end
|
||
|
||
function Tweaks:SetMouseoverCast(enabled)
|
||
if enabled and not mouseoverCastEnabled then
|
||
InitMouseoverCast()
|
||
elseif not enabled and mouseoverCastEnabled then
|
||
if origUseAction then UseAction = origUseAction end
|
||
if origCastSpellByName then CastSpellByName = origCastSpellByName end
|
||
mouseoverCastEnabled = false
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami] 鼠标指向施法已关闭|r")
|
||
end
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Module API
|
||
--------------------------------------------------------------------------------
|
||
function Tweaks:Initialize()
|
||
local cfg = GetTweaksCfg()
|
||
|
||
if cfg.autoStance ~= false then
|
||
local ok, err = pcall(InitAutoStance)
|
||
if not ok then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: AutoStance init failed: " .. tostring(err) .. "|r")
|
||
end
|
||
end
|
||
|
||
if cfg.superWoW ~= false then
|
||
local ok, err = pcall(InitSuperWoW)
|
||
if not ok then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: SuperWoW init failed: " .. tostring(err) .. "|r")
|
||
end
|
||
end
|
||
|
||
if cfg.turtleCompat ~= false then
|
||
local ok, err = pcall(InitTurtleCompat)
|
||
if not ok then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: TurtleCompat init failed: " .. tostring(err) .. "|r")
|
||
end
|
||
end
|
||
|
||
if SFrames.WorldMap and SFrames.WorldMap.initialized then
|
||
-- New WorldMap module has taken over; skip legacy code
|
||
elseif cfg.worldMapWindow ~= false then
|
||
local ok, err = pcall(InitWorldMapWindow)
|
||
if not ok then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: WorldMapWindow init failed: " .. tostring(err) .. "|r")
|
||
end
|
||
end
|
||
|
||
if cfg.cooldownNumbers ~= false then
|
||
local ok, err = pcall(InitCooldownNumbers)
|
||
if not ok then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: CooldownNumbers init failed: " .. tostring(err) .. "|r")
|
||
end
|
||
end
|
||
|
||
do
|
||
local ok, err = pcall(InitQuestWatchCountdown)
|
||
if not ok then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: QuestWatchCountdown init failed: " .. tostring(err) .. "|r")
|
||
end
|
||
end
|
||
|
||
if cfg.hunterAspectGuard ~= false then
|
||
local ok, err = pcall(InitHunterAspectGuard)
|
||
if not ok then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: HunterAspectGuard init failed: " .. tostring(err) .. "|r")
|
||
end
|
||
end
|
||
|
||
if cfg.combatNotify ~= false then
|
||
local ok, err = pcall(InitCombatNotify)
|
||
if not ok then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: CombatNotify init failed: " .. tostring(err) .. "|r")
|
||
end
|
||
end
|
||
|
||
if cfg.mouseoverCast then
|
||
local ok, err = pcall(InitMouseoverCast)
|
||
if not ok then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: MouseoverCast init failed: " .. tostring(err) .. "|r")
|
||
end
|
||
end
|
||
|
||
if cfg.darkUI then
|
||
local ok, err = pcall(InitDarkUI)
|
||
if not ok then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: DarkUI init failed: " .. tostring(err) .. "|r")
|
||
end
|
||
end
|
||
|
||
do
|
||
local ok, err = pcall(InitPopupSkin)
|
||
if not ok then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: PopupSkin init failed: " .. tostring(err) .. "|r")
|
||
end
|
||
end
|
||
end
|