-------------------------------------------------------------------------------- -- 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: -- Temporarily try the mouseover unit first while preserving the original -- target. If the spell cannot resolve on mouseover, stop the pending target -- mode, restore the original target, and retry there. -------------------------------------------------------------------------------- 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 CaptureTargetState() local hadTarget = UnitExists("target") return { hadTarget = hadTarget, name = hadTarget and UnitName("target") or nil, } end local function RestoreTargetState(state) if not state then return end if state.hadTarget and state.name then TargetLastTarget() if not UnitExists("target") or UnitName("target") ~= state.name then TargetByName(state.name, true) end else ClearTarget() end end local function ResolvePendingSpellTarget(unit) if not (SpellIsTargeting and SpellIsTargeting()) then return true end if unit then if SpellCanTargetUnit then if SpellCanTargetUnit(unit) then SpellTargetUnit(unit) end else SpellTargetUnit(unit) end end if SpellIsTargeting and SpellIsTargeting() then return false end return true end local function TryActionOnUnit(unit, action, cursor, onSelf) if not unit then return false end if not (UnitIsUnit and UnitExists("target") and UnitIsUnit(unit, "target")) then TargetUnit(unit) end origUseAction(action, cursor, onSelf) return ResolvePendingSpellTarget(unit) end local function TryCastSpellOnUnit(unit, spell) if not unit then return false end if not (UnitIsUnit and UnitExists("target") and UnitIsUnit(unit, "target")) then TargetUnit(unit) end origCastSpellByName(spell) return ResolvePendingSpellTarget(unit) 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 local prevTarget = CaptureTargetState() inMouseoverAction = true local castOnMouseover = TryActionOnUnit(moUnit, action, cursor, onSelf) if not castOnMouseover and SpellIsTargeting and SpellIsTargeting() then SpellStopTargeting() end RestoreTargetState(prevTarget) if not castOnMouseover and prevTarget.hadTarget then origUseAction(action, cursor, onSelf) if SpellIsTargeting and SpellIsTargeting() then if not ResolvePendingSpellTarget("target") then SpellStopTargeting() end end 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 local prevTarget = CaptureTargetState() inMouseoverAction = true local castOnMouseover = TryCastSpellOnUnit(moUnit, spell) if not castOnMouseover and SpellIsTargeting and SpellIsTargeting() then SpellStopTargeting() end RestoreTargetState(prevTarget) if not castOnMouseover and prevTarget.hadTarget then origCastSpellByName(spell) if SpellIsTargeting and SpellIsTargeting() then if not ResolvePendingSpellTarget("target") then SpellStopTargeting() end end end inMouseoverAction = false 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