-------------------------------------------------------------------------------- -- 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. Auto Dismount - cancel shapeshift/mount when casting incompatible spells -- 8. Hunter Aspect Guard - auto switch to Hawk when taking damage with Cheetah/Pack -------------------------------------------------------------------------------- SFrames.Tweaks = SFrames.Tweaks or {} local Tweaks = SFrames.Tweaks SFrames.castdb = SFrames.castdb or {} 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 } 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 -------------------------------------------------------------------------------- -- Auto Dismount / Cancel Shapeshift -- When casting a spell that fails because you are mounted or shapeshifted, -- automatically cancel the mount/shapeshift buff so the next cast succeeds. -------------------------------------------------------------------------------- local function InitAutoDismount() local dismount = CreateFrame("Frame", "NanamiAutoDismount") local _, playerClass = UnitClass("player") local scanner = CreateFrame("GameTooltip", "NanamiDismountScan", nil, "GameTooltipTemplate") scanner:SetOwner(WorldFrame, "ANCHOR_NONE") scanner:SetAlpha(0) scanner:Hide() local mountStrings = { "^Increases speed by (.+)%%", "^Erhöht Tempo um (.+)%%", "^Aumenta la velocidad en un (.+)%%", "^Augmente la vitesse de (.+)%%", "^Скорость увеличена на (.+)%%", "^이동 속도 (.+)%%만큼 증가", "^速度提高(.+)%%", "^移动速度提高(.+)%%", "speed based on", "Slow and steady...", "Riding", "Lento y constante...", "Aumenta la velocidad según tu habilidad de Montar.", "根据您的骑行技能提高速度。", "根据骑术技能提高速度。", "又慢又稳......", } local shapeshiftIcons = { "ability_racial_bearform", "ability_druid_catform", "ability_druid_travelform", "spell_nature_forceofnature", "ability_druid_aquaticform", "spell_nature_spiritwolf", "ability_druid_treeoflife", "ability_druid_stagform", } local hunterAspectIcons = { "ability_mount_jungletiger", "ability_mount_packhorse", } local errorStrings = {} local errorGlobals = { "SPELL_FAILED_NOT_MOUNTED", "ERR_ATTACK_MOUNTED", "ERR_TAXIPLAYERALREADYMOUNTED", "ERR_NOT_WHILE_MOUNTED", "SPELL_FAILED_NOT_SHAPESHIFT", "SPELL_FAILED_NO_ITEMS_WHILE_SHAPESHIFTED", "SPELL_NOT_SHAPESHIFTED", "SPELL_NOT_SHAPESHIFTED_NOSPACE", "ERR_CANT_INTERACT_SHAPESHIFTED", "ERR_NOT_WHILE_SHAPESHIFTED", "ERR_NO_ITEMS_WHILE_SHAPESHIFTED", "ERR_TAXIPLAYERSHAPESHIFTED", "ERR_MOUNT_SHAPESHIFTED", } for _, name in pairs(errorGlobals) do local val = getfenv(0)[name] if val then table.insert(errorStrings, val) end end dismount:RegisterEvent("UI_ERROR_MESSAGE") dismount:SetScript("OnEvent", function() if arg1 == SPELL_FAILED_NOT_STANDING then SitOrStand() return end local matched = false for _, err in pairs(errorStrings) do if arg1 == err then matched = true break end end if not matched then return end for i = 0, 31 do local buff = GetPlayerBuffTexture(i) if buff then local lowerBuff = string.lower(buff) local skip = false if playerClass == "HUNTER" then for _, tex in pairs(hunterAspectIcons) do if string.find(lowerBuff, tex) then skip = true break end end end if not skip then scanner:ClearLines() scanner:SetPlayerBuff(i) for line = 1, scanner:NumLines() do local text = getfenv(0)["NanamiDismountScanTextLeft" .. line] if text and text:GetText() then for _, str in pairs(mountStrings) do if string.find(text:GetText(), str) then CancelPlayerBuff(i) return end end end end for _, icon in pairs(shapeshiftIcons) do if string.find(lowerBuff, icon) then CancelPlayerBuff(i) return end end if string.find(lowerBuff, "ability_mount_") then CancelPlayerBuff(i) return end end 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 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 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 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 cooldown.cooldowntext:SetScript("OnUpdate", CooldownOnUpdate) 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() else if frame.cooldownmask then frame.cooldownmask:Hide() end frame.cooldowntext:Hide() end frame.cooldowntext.start = start frame.cooldowntext.duration = duration 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 with Aspect of the Cheetah or Aspect of the Pack -- active, automatically cancel the aspect to prevent repeated dazing. -------------------------------------------------------------------------------- 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 then if GetTime() - lastCancel >= 1.0 then if CancelDangerousAspect() then lastCancel = GetTime() end end end lastHP = hp 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.autoDismount ~= false then local ok, err = pcall(InitAutoDismount) if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: AutoDismount 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.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