Files
Nanami-UI/WorldMap.lua
2026-03-31 18:03:23 +08:00

2121 lines
76 KiB
Lua

--------------------------------------------------------------------------------
-- Nanami-UI: WorldMap Module (WorldMap.lua)
-- Full Nanami-UI themed world map: windowed, dark pink-purple skin,
-- hides all Blizzard decorations, custom title bar + close button,
-- brute-force tooltip theming while the map is open.
--------------------------------------------------------------------------------
SFrames = SFrames or {}
SFrames.WorldMap = {}
local WM = SFrames.WorldMap
--------------------------------------------------------------------------------
-- Nanami theme constants (matches SocialUI / ConfigUI / QuestUI)
--------------------------------------------------------------------------------
local PANEL_BACKDROP = {
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 },
}
local _A = SFrames.ActiveTheme
local TOOLTIP_BACKDROP = {
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 },
}
--------------------------------------------------------------------------------
-- Config
--------------------------------------------------------------------------------
function WM:GetConfig()
SFramesDB = SFramesDB or {}
if type(SFramesDB.WorldMap) ~= "table" then
SFramesDB.WorldMap = { enabled = true, scale = 0.85 }
end
if SFramesDB.WorldMap.enabled == nil then
SFramesDB.WorldMap.enabled = true
end
return SFramesDB.WorldMap
end
--------------------------------------------------------------------------------
-- HookScript helper
--------------------------------------------------------------------------------
local function HookScript(frame, script, fn)
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
fn(a1,a2,a3,a4,a5,a6,a7,a8,a9)
end)
end
--------------------------------------------------------------------------------
-- 1. Hide Blizzard Decorations
-- Called at init AND on every WorldMapFrame:OnShow to counter Blizzard resets
--------------------------------------------------------------------------------
local function NukeFrame(f)
if not f then return end
f:Hide()
f:SetAlpha(0)
f:EnableMouse(false)
f:ClearAllPoints()
f:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -9999, 9999)
f:SetWidth(1)
f:SetHeight(1)
if not f._nanamiNuked then
f._nanamiNuked = true
f.Show = function(self) self:Hide() end
if f.SetScript then
f:SetScript("OnShow", function() this:Hide() end)
end
end
end
local function HideAllRegions(frame)
if not frame or not frame.GetRegions then return end
local regions = { frame:GetRegions() }
for _, r in ipairs(regions) do
if r and r.Hide then r:Hide() end
end
end
local blizzHooked = false
local function NukeBorderChildren(border)
if not border or not border.GetChildren then return end
local children = { border:GetChildren() }
for _, child in ipairs(children) do
local name = child.GetName and child:GetName() or ""
local isDropDown = name ~= "" and string.find(name, "DropDown")
if not isDropDown then
NukeFrame(child)
end
end
end
local function HideBlizzardDecorations()
if BlackoutWorld then BlackoutWorld:Hide() end
local _G = getfenv(0)
local framesToNuke = {
"WorldMapFrameCloseButton",
"WorldMapFrameSizeUpButton",
"WorldMapFrameSizeDownButton",
}
for _, name in ipairs(framesToNuke) do
NukeFrame(_G[name])
end
HideAllRegions(WorldMapFrame)
local borderNames = {
"WorldMapFrameMiniBorderLeft",
"WorldMapFrameMiniBorderRight",
}
for _, name in ipairs(borderNames) do
local border = _G[name]
if border then
HideAllRegions(border)
NukeBorderChildren(border)
border:EnableMouse(false)
if not blizzHooked then
local oldShow = border.Show
if oldShow then
border.Show = function(self)
oldShow(self)
HideAllRegions(self)
NukeBorderChildren(self)
self:EnableMouse(false)
end
end
end
end
end
if WorldMapFrameTitle then
WorldMapFrameTitle:Hide()
end
for _, n in ipairs({"WorldMapFrameSizeUpButton", "WorldMapFrameSizeDownButton"}) do
local f = _G[n]
if f then
f:Hide()
f:SetAlpha(0)
f:EnableMouse(false)
if f.ClearAllPoints then
f:ClearAllPoints()
f:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -9999, 9999)
end
if f.GetRegions then
local regs = { f:GetRegions() }
for _, r in ipairs(regs) do
if r and r.Hide then r:Hide() end
end
end
end
end
if WorldMapFrame.GetChildren then
local wmCh = { WorldMapFrame:GetChildren() }
for _, c in ipairs(wmCh) do
if c and not c._nanamiNuked and not c._nanamiSkinned then
local cn = c.GetName and c:GetName() or ""
if cn ~= "" and (
string.find(cn, "SizeUp") or
string.find(cn, "SizeDown") or
string.find(cn, "Resize") or
string.find(cn, "Maximize") or
string.find(cn, "Minimize")
) then
NukeFrame(c)
elseif cn == "" then
local w = c.GetWidth and c:GetWidth() or 999
local h = c.GetHeight and c:GetHeight() or 999
if w > 0 and w < 30 and h > 0 and h < 30 then
NukeFrame(c)
end
end
end
end
end
blizzHooked = true
end
--------------------------------------------------------------------------------
-- 2. Create Nanami Skin
--------------------------------------------------------------------------------
local skinFrame, titleBar, titleText, closeBtn, coordText, mouseCoordText, coordOverlay
local function CreateNanamiSkin()
if skinFrame then return end
local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
skinFrame = CreateFrame("Frame", "NanamiWorldMapSkin", WorldMapFrame)
skinFrame:SetFrameLevel(WorldMapFrame:GetFrameLevel())
skinFrame:SetAllPoints(WorldMapFrame)
skinFrame:SetBackdrop(PANEL_BACKDROP)
skinFrame:SetBackdropColor(unpack(_A.panelBg))
skinFrame:SetBackdropBorderColor(unpack(_A.panelBorder))
titleBar = CreateFrame("Frame", nil, skinFrame)
titleBar:SetHeight(26)
titleBar:SetPoint("TOPLEFT", skinFrame, "TOPLEFT", 4, -4)
titleBar:SetPoint("TOPRIGHT", skinFrame, "TOPRIGHT", -4, -4)
titleBar:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
titleBar:SetBackdropColor(unpack(_A.headerBg))
titleBar:SetBackdropBorderColor(unpack(_A.panelBorder))
local titleIco = SFrames:CreateIcon(titleBar, "worldmap", 14)
titleIco:SetDrawLayer("OVERLAY")
titleIco:SetPoint("LEFT", titleBar, "LEFT", 10, 0)
titleIco:SetVertexColor(unpack(_A.title))
titleText = titleBar:CreateFontString(nil, "OVERLAY")
titleText:SetFont(font, 13, "OUTLINE")
titleText:SetPoint("LEFT", titleIco, "RIGHT", 5, 0)
titleText:SetTextColor(unpack(_A.title))
titleText:SetText("世界地图")
local hintText = titleBar:CreateFontString(nil, "OVERLAY")
hintText:SetFont(font, 10, "")
hintText:SetPoint("RIGHT", titleBar, "RIGHT", -40, 0)
hintText:SetTextColor(unpack(_A.dimText))
hintText:SetText("Ctrl+滚轮缩放 | Shift+滚轮透明度 | Ctrl+左键标记")
closeBtn = CreateFrame("Button", nil, titleBar)
closeBtn:SetWidth(22)
closeBtn:SetHeight(22)
closeBtn:SetPoint("RIGHT", titleBar, "RIGHT", -2, 0)
closeBtn:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
closeBtn:SetBackdropColor(unpack(_A.buttonDownBg))
closeBtn:SetBackdropBorderColor(unpack(_A.btnBorder))
local closeIcon = closeBtn:CreateTexture(nil, "OVERLAY")
closeIcon:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon")
closeIcon:SetTexCoord(0.25, 0.375, 0, 0.125)
closeIcon:SetPoint("CENTER", 0, 0)
closeIcon:SetWidth(14)
closeIcon:SetHeight(14)
closeBtn:SetScript("OnClick", function()
WorldMapFrame:Hide()
end)
closeBtn:SetScript("OnEnter", function()
this:SetBackdropColor(unpack(_A.btnHoverBg))
this:SetBackdropBorderColor(unpack(_A.btnHoverBd))
end)
closeBtn:SetScript("OnLeave", function()
this:SetBackdropColor(unpack(_A.buttonDownBg))
this:SetBackdropBorderColor(unpack(_A.btnBorder))
end)
-- High-level overlay for coordinates so they render above map content
coordOverlay = CreateFrame("Frame", "NanamiWorldMapCoordOverlay", WorldMapFrame)
coordOverlay:SetAllPoints(skinFrame)
coordOverlay:SetFrameStrata("TOOLTIP")
coordOverlay:SetFrameLevel(250)
coordOverlay:EnableMouse(false)
coordText = coordOverlay:CreateFontString(nil, "OVERLAY")
coordText:SetFont(font, 12, "OUTLINE")
coordText:SetPoint("BOTTOMLEFT", skinFrame, "BOTTOMLEFT", 12, 6)
coordText:SetTextColor(unpack(_A.title))
coordText:SetText("")
mouseCoordText = coordOverlay:CreateFontString(nil, "OVERLAY")
mouseCoordText:SetFont(font, 12, "OUTLINE")
mouseCoordText:SetPoint("BOTTOMRIGHT", skinFrame, "BOTTOMRIGHT", -12, 6)
mouseCoordText:SetTextColor(unpack(_A.dimText))
mouseCoordText:SetText("")
end
local function UpdateTitle()
if not titleText then return end
local zoneName = GetZoneText and GetZoneText() or ""
local subZone = GetSubZoneText and GetSubZoneText() or ""
local display = zoneName
if subZone ~= "" and subZone ~= zoneName then
display = zoneName .. " - " .. subZone
end
if display == "" then display = "世界地图" end
titleText:SetText(display)
end
local function UpdateCoords()
if not coordText then return end
local x, y = GetPlayerMapPosition("player")
if x and y and (x > 0 or y > 0) then
coordText:SetText(string.format("玩家: %.1f, %.1f", x * 100, y * 100))
else
coordText:SetText("")
end
end
local function UpdateMouseCoords()
if not mouseCoordText or not WorldMapButton then return end
if not WorldMapFrame or not WorldMapFrame:IsVisible() then
mouseCoordText:SetText("")
return
end
local cx, cy = GetCursorPosition()
if not cx or not cy then
mouseCoordText:SetText("")
return
end
local scale = WorldMapButton:GetEffectiveScale()
local left = WorldMapButton:GetLeft()
local top = WorldMapButton:GetTop()
local w = WorldMapButton:GetWidth()
local h = WorldMapButton:GetHeight()
if not left or not top or not w or not h or w == 0 or h == 0 then
mouseCoordText:SetText("")
return
end
local mx = (cx / scale - left) / w
local my = (top - cy / scale) / h
if mx >= 0 and mx <= 1 and my >= 0 and my <= 1 then
mouseCoordText:SetText(string.format("鼠标: %.1f, %.1f", mx * 100, my * 100))
else
mouseCoordText:SetText("")
end
end
--------------------------------------------------------------------------------
-- 3. Tooltip theming
-- SetBackdrop on scaled child frames is unreliable in WoW 1.12.
-- Approach: use a SEPARATE frame parented to UIParent (outside the
-- WorldMapFrame scale chain) positioned exactly behind whichever tooltip
-- is currently visible. This frame has its own backdrop at TOOLTIP strata.
-- Also hook WorldMapTooltip:Show to try SetBackdrop as secondary attempt.
--------------------------------------------------------------------------------
local ttBG
local TT_PAD = 4
local function CreateTooltipBG()
if ttBG then return end
ttBG = CreateFrame("Frame", "NanamiMapTTBG", UIParent)
ttBG:SetFrameStrata("FULLSCREEN_DIALOG")
ttBG:SetFrameLevel(255)
ttBG:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
ttBG:SetBackdropColor(unpack(_A.panelBg))
ttBG:SetBackdropBorderColor(unpack(_A.panelBorder))
ttBG:Hide()
end
local function PositionTTBG(tip)
if not ttBG then return end
if not tip or not tip:IsVisible() then
ttBG:Hide()
return
end
local l = tip:GetLeft()
local r = tip:GetRight()
local t = tip:GetTop()
local b = tip:GetBottom()
if not l or not r or not t or not b then
ttBG:Hide()
return
end
local tipScale = tip:GetEffectiveScale()
local bgScale = ttBG:GetEffectiveScale()
local s = tipScale / bgScale
ttBG:ClearAllPoints()
ttBG:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", l * s - TT_PAD, t * s + TT_PAD)
ttBG:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMLEFT", r * s + TT_PAD, b * s - TT_PAD)
ttBG:Show()
end
local function HideTTBG()
if ttBG then ttBG:Hide() end
end
local function RaiseTooltipStrata(tip)
if not tip then return end
tip:SetFrameStrata("TOOLTIP")
end
local function HookTooltipShow(tip)
if not tip or tip._nanamiShowHooked then return end
tip._nanamiShowHooked = true
local origShow = tip.Show
tip.Show = function(self)
origShow(self)
if WorldMapFrame and WorldMapFrame:IsVisible() then
self:SetFrameStrata("TOOLTIP")
self:SetBackdrop(TOOLTIP_BACKDROP)
self:SetBackdropColor(unpack(_A.panelBg))
self:SetBackdropBorderColor(unpack(_A.panelBorder))
end
end
end
local RepositionWorldMapControls
--------------------------------------------------------------------------------
-- 4. Per-frame updater
--------------------------------------------------------------------------------
local elapsed = 0
local function OnFrameUpdate()
if not WorldMapFrame or not WorldMapFrame:IsShown() then
HideTTBG()
return
end
local activeTip = nil
if WorldMapTooltip and WorldMapTooltip:IsVisible() then
activeTip = WorldMapTooltip
elseif GameTooltip and GameTooltip:IsVisible() then
activeTip = GameTooltip
end
if activeTip then
PositionTTBG(activeTip)
RaiseTooltipStrata(activeTip)
activeTip:SetBackdrop(TOOLTIP_BACKDROP)
activeTip:SetBackdropColor(unpack(_A.panelBg))
activeTip:SetBackdropBorderColor(unpack(_A.panelBorder))
else
HideTTBG()
end
UpdateMouseCoords()
elapsed = elapsed + (arg1 or 0)
if elapsed > 0.1 then
elapsed = 0
UpdateTitle()
UpdateCoords()
HideBlizzardDecorations()
RepositionWorldMapControls()
end
end
--------------------------------------------------------------------------------
-- 5. Layout
--------------------------------------------------------------------------------
local function ApplyLayout(cfg)
local s = cfg.scale or 0.85
WorldMapFrame:SetMovable(true)
WorldMapFrame:EnableMouse(true)
WorldMapFrame:SetScale(s)
WorldMapFrame:ClearAllPoints()
WorldMapFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 30)
WorldMapFrame:SetWidth(WorldMapButton:GetWidth() + 12)
WorldMapFrame:SetHeight(WorldMapButton:GetHeight() + 40)
WorldMapDetailFrame:ClearAllPoints()
WorldMapDetailFrame:SetPoint("TOPLEFT", WorldMapFrame, "TOPLEFT", 6, -32)
WorldMapButton:ClearAllPoints()
WorldMapButton:SetPoint("TOPLEFT", WorldMapDetailFrame, "TOPLEFT", 0, 0)
WorldMapButton:SetPoint("BOTTOMRIGHT", WorldMapDetailFrame, "BOTTOMRIGHT", 0, 0)
if BlackoutWorld then
BlackoutWorld:Hide()
end
end
local blizzLabelsKilled = false
local function KillBlizzardDropDownLabels()
if blizzLabelsKilled then return end
local _G = getfenv(0)
local targets = {
_G["WorldMapContinentDropDown"],
_G["WorldMapZoneDropDown"],
_G["WorldMapZoneMinimapDropDown"],
_G["WorldMapFrameMiniBorderLeft"],
_G["WorldMapFrameMiniBorderRight"],
WorldMapFrame,
}
local found = 0
for _, parent in ipairs(targets) do
if parent and parent.GetRegions then
local regions = { parent:GetRegions() }
for _, r in ipairs(regions) do
if r and not r._nanamiCustom and r.GetObjectType
and r:GetObjectType() == "FontString" then
local t = r.GetText and r:GetText() or ""
if t == "大陆" or t == "地区"
or t == "Continent" or t == "Zone" then
r:SetText("")
r:SetAlpha(0)
r:Hide()
r.Show = function() end
r.SetText = function() end
r.SetAlpha = function() end
found = found + 1
end
end
end
end
end
for _, n in ipairs({"WorldMapContinentDropDownLabel", "WorldMapZoneDropDownLabel", "WorldMapZoneMinimapDropDownLabel"}) do
local lbl = _G[n]
if lbl then
lbl:SetAlpha(0)
lbl:Hide()
lbl.Show = function() end
lbl.SetText = function() end
found = found + 1
end
end
if found > 0 then blizzLabelsKilled = true end
end
RepositionWorldMapControls = function()
if not skinFrame or not titleBar then return end
local _G = getfenv(0)
if not blizzLabelsKilled then KillBlizzardDropDownLabels() end
if WorldMapDetailFrame then
WorldMapDetailFrame:ClearAllPoints()
WorldMapDetailFrame:SetPoint("TOPLEFT", WorldMapFrame, "TOPLEFT", 6, -32)
end
if WorldMapButton and WorldMapDetailFrame then
WorldMapButton:ClearAllPoints()
WorldMapButton:SetPoint("TOPLEFT", WorldMapDetailFrame, "TOPLEFT", 0, 0)
WorldMapButton:SetPoint("BOTTOMRIGHT", WorldMapDetailFrame, "BOTTOMRIGHT", 0, 0)
end
local cDD = _G["WorldMapContinentDropDown"]
local zDD = _G["WorldMapZoneDropDown"]
local zBtn = _G["WorldMapZoomOutButton"]
if cDD then
cDD:ClearAllPoints()
cDD:SetPoint("TOPLEFT", skinFrame, "TOPLEFT", 200, -3)
end
if zDD then
zDD:ClearAllPoints()
zDD:SetPoint("LEFT", cDD, "RIGHT", 0, 0)
end
if zBtn then
zBtn:ClearAllPoints()
zBtn:SetPoint("LEFT", zDD, "RIGHT", -14, 2)
zBtn:SetWidth(48)
zBtn:SetHeight(20)
end
end
--------------------------------------------------------------------------------
-- 6. Window mode setup
--------------------------------------------------------------------------------
function WM:SetupWindowMode()
if Cartographer or METAMAP_TITLE then return end
local cfg = self:GetConfig()
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)
local c = WM:GetConfig()
WorldMapFrame:SetScale(c.scale or 0.85)
WorldMapFrame:SetAlpha(1)
WorldMapFrame:SetFrameStrata("FULLSCREEN_DIALOG")
if BlackoutWorld then BlackoutWorld:Hide() end
HideBlizzardDecorations()
RepositionWorldMapControls()
end)
HookScript(WorldMapFrame, "OnHide", function()
HideTTBG()
end)
HookScript(WorldMapFrame, "OnMouseWheel", function()
if IsShiftKeyDown() then
local a = WorldMapFrame:GetAlpha() + arg1 / 10
if a < 0.2 then a = 0.2 end
if a > 1 then a = 1 end
WorldMapFrame:SetAlpha(a)
elseif IsControlKeyDown() then
local oldScale = WorldMapFrame:GetScale()
local newScale = oldScale + arg1 / 10
if newScale < 0.4 then newScale = 0.4 end
if newScale > 1.5 then newScale = 1.5 end
local eff = WorldMapFrame:GetEffectiveScale()
local fw = WorldMapFrame:GetWidth()
local fh = WorldMapFrame:GetHeight()
local fl = WorldMapFrame:GetLeft()
local ft = WorldMapFrame:GetTop()
if fl and ft and fw and fh and eff and eff > 0 then
local scrCX = (fl + fw / 2) * eff
local scrCY = (ft - fh / 2) * eff
WorldMapFrame:SetScale(newScale)
local newEff = newScale * eff / oldScale
WorldMapFrame:ClearAllPoints()
WorldMapFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT",
scrCX / newEff - fw / 2,
scrCY / newEff + fh / 2)
else
WorldMapFrame:SetScale(newScale)
end
local c = WM:GetConfig()
c.scale = newScale
end
end)
HookScript(WorldMapFrame, "OnMouseDown", function()
WorldMapFrame:StartMoving()
end)
HookScript(WorldMapFrame, "OnMouseUp", function()
WorldMapFrame:StopMovingOrSizing()
end)
ApplyLayout(cfg)
local _G2 = getfenv(0)
local origMaximize = _G2.WorldMapFrame_Maximize
local origMinimize = _G2.WorldMapFrame_Minimize
_G2.WorldMapFrame_Maximize = function()
if origMaximize then origMaximize() end
ApplyLayout(WM:GetConfig())
HideBlizzardDecorations()
RepositionWorldMapControls()
end
_G2.WorldMapFrame_Minimize = function()
if origMinimize then origMinimize() end
ApplyLayout(WM:GetConfig())
HideBlizzardDecorations()
RepositionWorldMapControls()
end
end
--------------------------------------------------------------------------------
-- 7. Skin World Map Native Controls (Dropdowns, Buttons)
--------------------------------------------------------------------------------
local function SkinDropDown(dropdownName)
local _G = getfenv(0)
local dd = _G[dropdownName]
if not dd or dd._nanamiSkinned then return end
dd._nanamiSkinned = true
for _, suffix in ipairs({ "Left", "Middle", "Right" }) do
local tex = _G[dropdownName .. suffix]
if tex then tex:SetAlpha(0) end
end
local bg = CreateFrame("Frame", nil, dd)
bg:SetPoint("TOPLEFT", dd, "TOPLEFT", 16, -2)
bg:SetPoint("BOTTOMRIGHT", dd, "BOTTOMRIGHT", -16, 6)
bg:SetFrameLevel(dd:GetFrameLevel())
bg:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
bg:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
bg:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
dd._nanamiBG = bg
local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
local text = _G[dropdownName .. "Text"]
if text then
text:SetFont(font, 11, "OUTLINE")
text:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3])
end
local origLabel = _G[dropdownName .. "Label"]
local labelText = ""
if origLabel then
labelText = origLabel:GetText() or ""
origLabel:Hide()
origLabel:SetAlpha(0)
origLabel:SetText("")
origLabel:ClearAllPoints()
origLabel:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -9999, 9999)
origLabel.Show = function() end
origLabel.SetText = function() end
origLabel.SetPoint = function() end
origLabel.ClearAllPoints = function() end
end
if labelText == "" then
if string.find(dropdownName, "Continent") then labelText = "大陆"
elseif string.find(dropdownName, "ZoneMinimap") then labelText = ""
elseif string.find(dropdownName, "Zone") then labelText = "地区"
end
end
if labelText ~= "" then
local newLabel = bg:CreateFontString(nil, "OVERLAY")
newLabel._nanamiCustom = true
newLabel:SetFont(font, 11, "OUTLINE")
newLabel:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3])
newLabel:SetPoint("RIGHT", bg, "LEFT", -4, 0)
newLabel:SetText(labelText)
dd._nanamiLabel = newLabel
end
local btn = _G[dropdownName .. "Button"]
if btn then
local nt = btn:GetNormalTexture()
if nt then nt:SetVertexColor(_A.title[1], _A.title[2], _A.title[3]) end
local pt = btn:GetPushedTexture()
if pt then pt:SetVertexColor(_A.title[1], _A.title[2], _A.title[3]) end
local dt = btn:GetDisabledTexture()
if dt then dt:SetVertexColor(_A.dimText[1], _A.dimText[2], _A.dimText[3]) end
local ht = btn:GetHighlightTexture()
if ht then ht:SetVertexColor(_A.title[1], _A.title[2], _A.title[3], 0.3) end
end
end
local function SkinDropDownLists()
local _G = getfenv(0)
for i = 1, 3 do
local listName = "DropDownList" .. i
local backdrop = _G[listName .. "Backdrop"]
if backdrop then
backdrop:SetBackdrop(PANEL_BACKDROP)
backdrop:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4])
backdrop:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4])
end
local menuBackdrop = _G[listName .. "MenuBackdrop"]
if menuBackdrop then
menuBackdrop:SetBackdrop(PANEL_BACKDROP)
menuBackdrop:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4])
menuBackdrop:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4])
end
for j = 1, 30 do
local hlTex = _G[listName .. "Button" .. j .. "Highlight"]
if hlTex and hlTex.SetVertexColor then
hlTex:SetVertexColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], 0.35)
end
end
end
end
local function SkinNativeButton(btnName, labelOverride)
local _G = getfenv(0)
local btn = _G[btnName]
if not btn or btn._nanamiSkinned then return end
btn._nanamiSkinned = true
local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
local regions = { btn:GetRegions() }
for _, r in ipairs(regions) do
if r and r.GetObjectType 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
local nt = btn.GetNormalTexture and btn:GetNormalTexture()
if nt then nt:SetAlpha(0) end
local pt = btn.GetPushedTexture and btn:GetPushedTexture()
if pt then pt:SetAlpha(0) end
local ht = btn.GetHighlightTexture and btn:GetHighlightTexture()
if ht then ht:SetAlpha(0) end
btn:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
btn:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
btn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
if labelOverride then
local fs = btn:GetFontString() or btn:CreateFontString(nil, "OVERLAY")
fs:SetFont(font, 11, "OUTLINE")
fs:SetText(labelOverride)
fs:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3])
fs:SetPoint("CENTER", 0, 0)
elseif btn.GetFontString and btn:GetFontString() then
local fs = btn:GetFontString()
fs:SetFont(font, 11, "OUTLINE")
fs:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3])
end
local prevEnter = btn:GetScript("OnEnter")
local prevLeave = btn:GetScript("OnLeave")
btn:SetScript("OnEnter", function()
if prevEnter then prevEnter() end
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4])
end)
btn:SetScript("OnLeave", function()
if prevLeave then prevLeave() end
this:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
this:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
end)
end
local ddListHooked = false
local function SkinWorldMapControls()
SkinDropDown("WorldMapContinentDropDown")
SkinDropDown("WorldMapZoneDropDown")
SkinDropDown("WorldMapZoneMinimapDropDown")
SkinNativeButton("WorldMapZoomOutButton", "缩小")
SkinDropDownLists()
RepositionWorldMapControls()
if not ddListHooked then
ddListHooked = true
local _G = getfenv(0)
for i = 1, 3 do
local list = _G["DropDownList" .. i]
if list then
local oldShow = list.Show
if oldShow then
list.Show = function(self)
oldShow(self)
SkinDropDownLists()
end
end
end
end
end
end
--------------------------------------------------------------------------------
-- 8. Waypoint / Map Pin System
--------------------------------------------------------------------------------
local waypoint = { active = false, continent = 0, zone = 0, x = 0, y = 0, name = "" }
local pinFrame, pinTexture, pinLabel, pinShareBtn, pinClearBtn
local function GetMapCursorPos()
if not WorldMapButton then return nil, nil end
local cx, cy = GetCursorPosition()
if not cx or not cy then return nil, nil end
local scale = WorldMapButton:GetEffectiveScale()
local left = WorldMapButton:GetLeft()
local top = WorldMapButton:GetTop()
local w = WorldMapButton:GetWidth()
local h = WorldMapButton:GetHeight()
if not left or not top or not w or not h or w == 0 or h == 0 then return nil, nil end
local mx = (cx / scale - left) / w
local my = (top - cy / scale) / h
if mx >= 0 and mx <= 1 and my >= 0 and my <= 1 then
return mx, my
end
return nil, nil
end
local function GetCurrentMapName()
local _G = getfenv(0)
local zoneText = _G["WorldMapFrameAreaLabel"]
if zoneText and zoneText.GetText then
local t = zoneText:GetText()
if t and t ~= "" then return t end
end
local c = GetCurrentMapContinent and GetCurrentMapContinent() or 0
local z = GetCurrentMapZone and GetCurrentMapZone() or 0
if z > 0 and GetMapZones then
local zones = { GetMapZones(c) }
if zones[z] then return zones[z] end
end
if c > 0 and GetMapContinents then
local continents = { GetMapContinents() }
if continents[c] then return continents[c] end
end
return "未知区域"
end
local function CreateWaypointPin()
if pinFrame then return end
local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
pinFrame = CreateFrame("Frame", "NanamiWorldMapPin", WorldMapButton)
pinFrame:SetWidth(24)
pinFrame:SetHeight(38)
pinFrame:SetFrameStrata("FULLSCREEN_DIALOG")
pinFrame:SetFrameLevel(200)
local stem = pinFrame:CreateTexture(nil, "ARTWORK")
stem:SetTexture("Interface\\Buttons\\WHITE8X8")
stem:SetWidth(2)
stem:SetHeight(14)
stem:SetPoint("BOTTOM", pinFrame, "BOTTOM", 0, 0)
stem:SetVertexColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], 0.85)
local glow = pinFrame:CreateTexture(nil, "BACKGROUND")
glow:SetTexture("Interface\\Buttons\\WHITE8X8")
glow:SetWidth(20)
glow:SetHeight(20)
glow:SetPoint("BOTTOM", stem, "TOP", 0, -6)
glow:SetVertexColor(_A.accent[1], _A.accent[2], _A.accent[3], 0.30)
pinTexture = pinFrame:CreateTexture(nil, "ARTWORK")
pinTexture:SetTexture("Interface\\Buttons\\WHITE8X8")
pinTexture:SetWidth(12)
pinTexture:SetHeight(12)
pinTexture:SetPoint("BOTTOM", stem, "TOP", 0, -1)
pinTexture:SetVertexColor(_A.accent[1], _A.accent[2], _A.accent[3], 1)
local dot = pinFrame:CreateTexture(nil, "OVERLAY")
dot:SetTexture("Interface\\Buttons\\WHITE8X8")
dot:SetWidth(4)
dot:SetHeight(4)
dot:SetPoint("CENTER", pinTexture, "CENTER", 0, 0)
dot:SetVertexColor(_A.title[1], _A.title[2], _A.title[3], 1)
pinLabel = pinFrame:CreateFontString(nil, "OVERLAY")
pinLabel:SetFont(font, 11, "OUTLINE")
pinLabel:SetPoint("TOP", pinFrame, "BOTTOM", 0, -2)
pinLabel:SetTextColor(_A.title[1], _A.title[2], _A.title[3])
pinLabel:SetText("")
local btnW, btnH = 52, 18
pinShareBtn = CreateFrame("Button", nil, pinFrame)
pinShareBtn:SetWidth(btnW)
pinShareBtn:SetHeight(btnH)
pinShareBtn:SetPoint("TOPLEFT", pinFrame, "BOTTOMLEFT", -10, -8)
pinShareBtn:SetBackdrop(PANEL_BACKDROP)
pinShareBtn:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
pinShareBtn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
local shareFS = pinShareBtn:CreateFontString(nil, "OVERLAY")
shareFS:SetFont(font, 10, "OUTLINE")
shareFS:SetPoint("CENTER", 0, 0)
shareFS:SetText("分享")
shareFS:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3])
pinShareBtn:SetScript("OnEnter", function()
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
end)
pinShareBtn:SetScript("OnLeave", function()
this:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
end)
pinShareBtn:SetScript("OnClick", function()
WM:ShareWaypoint()
end)
pinClearBtn = CreateFrame("Button", nil, pinFrame)
pinClearBtn:SetWidth(btnW)
pinClearBtn:SetHeight(btnH)
pinClearBtn:SetPoint("LEFT", pinShareBtn, "RIGHT", 4, 0)
pinClearBtn:SetBackdrop(PANEL_BACKDROP)
pinClearBtn:SetBackdropColor(_A.buttonDownBg[1], _A.buttonDownBg[2], _A.buttonDownBg[3], _A.buttonDownBg[4])
pinClearBtn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
local clearFS = pinClearBtn:CreateFontString(nil, "OVERLAY")
clearFS:SetFont(font, 10, "OUTLINE")
clearFS:SetPoint("CENTER", 0, 0)
clearFS:SetText("清除")
clearFS:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3])
pinClearBtn:SetScript("OnEnter", function()
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
end)
pinClearBtn:SetScript("OnLeave", function()
this:SetBackdropColor(_A.buttonDownBg[1], _A.buttonDownBg[2], _A.buttonDownBg[3], _A.buttonDownBg[4])
end)
pinClearBtn:SetScript("OnClick", function()
WM:ClearWaypoint()
end)
pinFrame:Hide()
end
local function UpdatePinPosition()
if not pinFrame or not waypoint.active then
if pinFrame then pinFrame:Hide() end
return
end
if not WorldMapButton or not WorldMapFrame:IsVisible() then
pinFrame:Hide()
return
end
local c = GetCurrentMapContinent and GetCurrentMapContinent() or 0
local z = GetCurrentMapZone and GetCurrentMapZone() or 0
if c ~= waypoint.continent or z ~= waypoint.zone then
pinFrame:Hide()
return
end
local w = WorldMapButton:GetWidth()
local h = WorldMapButton:GetHeight()
if not w or not h or w == 0 or h == 0 then
pinFrame:Hide()
return
end
pinFrame:ClearAllPoints()
pinFrame:SetPoint("BOTTOM", WorldMapButton, "TOPLEFT", w * waypoint.x, -h * waypoint.y)
pinFrame:Show()
end
function WM:SetWaypoint(continent, zone, x, y, name)
waypoint.active = true
waypoint.continent = continent
waypoint.zone = zone
waypoint.x = x
waypoint.y = y
waypoint.name = name or "标记点"
CreateWaypointPin()
local xPct = string.format("%.1f", x * 100)
local yPct = string.format("%.1f", y * 100)
pinLabel:SetText(waypoint.name .. " (" .. xPct .. ", " .. yPct .. ")")
UpdatePinPosition()
if SFrames and SFrames.Print then
SFrames:Print("地图标记已放置: " .. waypoint.name .. " (" .. xPct .. ", " .. yPct .. ")")
end
end
function WM:ClearWaypoint()
waypoint.active = false
waypoint.continent = 0
waypoint.zone = 0
waypoint.x = 0
waypoint.y = 0
waypoint.name = ""
if pinFrame then pinFrame:Hide() end
end
function WM:ShareWaypoint()
if not waypoint.active then return end
local xPct = string.format("%.1f", waypoint.x * 100)
local yPct = string.format("%.1f", waypoint.y * 100)
local shareText = "<npin:" .. waypoint.continent .. ":" .. waypoint.zone .. ":" .. xPct .. ":" .. yPct .. ":" .. waypoint.name .. ">"
if ChatFrameEditBox then
if not ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Show()
end
ChatFrameEditBox:SetFocus()
ChatFrameEditBox:SetText(shareText .. " " .. waypoint.name .. " (" .. xPct .. ", " .. yPct .. ")")
end
end
function WM:HandleWaypointLink(data)
local _, _, cs, zs, xs, ys = string.find(data, "^(%d+):(%d+):([%d%.]+):([%d%.]+)")
if not cs then return end
local c = tonumber(cs) or 0
local z = tonumber(zs) or 0
local x = (tonumber(xs) or 0) / 100
local y = (tonumber(ys) or 0) / 100
local name = "标记点"
if z > 0 and GetMapZones then
local zones = { GetMapZones(c) }
if zones[z] then name = zones[z] end
elseif c > 0 and GetMapContinents then
local continents = { GetMapContinents() }
if continents[c] then name = continents[c] end
end
if not WorldMapFrame:IsVisible() then
WorldMapFrame:Show()
end
self:SetWaypoint(c, z, x, y, name)
local pending = { continent = c, zone = z, x = x, y = y, name = name }
local timer = CreateFrame("Frame")
local waited = 0
timer:SetScript("OnUpdate", function()
waited = waited + (arg1 or 0)
if waited >= 0.05 then
timer:SetScript("OnUpdate", nil)
if SetMapZoom then
SetMapZoom(pending.continent, pending.zone)
end
WM:SetWaypoint(pending.continent, pending.zone, pending.x, pending.y, pending.name)
end
end)
end
--------------------------------------------------------------------------------
-- 8b. Darkmoon Faire Map Markers (暗月马戏团世界地图标记)
--------------------------------------------------------------------------------
local DMF_ICON = "Interface\\Icons\\INV_Misc_Orb_02"
local DMF_SIZE = 24
local DMF_SIZE_CONT = 16
local DMF_LOCATIONS = {
{ zone = "ElwynnForest", cont = 2,
zx = 0.41, zy = 0.69, -- zone-level coordinates
cx = 0.453, cy = 0.721, -- continent-level coordinates (Eastern Kingdoms)
name = "闪金镇", fullName = "闪金镇 (艾尔文森林)",
},
{ zone = "Mulgore", cont = 1,
zx = 0.36, zy = 0.38, -- zone-level coordinates
cx = 0.463, cy = 0.595, -- continent-level coordinates (Kalimdor)
name = "莫高雷", fullName = "莫高雷",
},
}
local DMF_REF_LOC = 1 -- ref Monday (2026-03-09) week → Goldshire
local dmfPins = {}
local dmfPulse = 0
local dmfRefJDN = nil
local function DiscoverDmfZoneIndices()
local savedC = GetCurrentMapContinent and GetCurrentMapContinent() or 0
local savedZ = GetCurrentMapZone and GetCurrentMapZone() or 0
for i = 1, 2 do
local loc = DMF_LOCATIONS[i]
if not loc.zoneIdx then
local target = string.lower(loc.zone)
local zones = { GetMapZones(loc.cont) }
for idx = 1, table.getn(zones) do
SetMapZoom(loc.cont, idx)
local mf = GetMapInfo and GetMapInfo() or ""
if mf ~= "" then
if mf == loc.zone then
loc.zoneIdx = idx
break
elseif string.find(string.lower(mf), target) then
loc.zoneIdx = idx
loc.zone = mf
break
elseif string.find(target, string.lower(mf)) then
loc.zoneIdx = idx
loc.zone = mf
break
end
end
end
end
end
if savedZ > 0 then
SetMapZoom(savedC, savedZ)
elseif savedC > 0 then
SetMapZoom(savedC, 0)
else
if SetMapToCurrentZone then SetMapToCurrentZone() end
end
end
local function DmfJDN(y, m, d)
local a = math.floor((14 - m) / 12)
local y2 = y + 4800 - a
local m2 = m + 12 * a - 3
return d + math.floor((153 * m2 + 2) / 5) + 365 * y2
+ math.floor(y2 / 4) - math.floor(y2 / 100) + math.floor(y2 / 400) - 32045
end
local function GetCETJDN()
local ts = time()
local daysSinceEpoch = math.floor(ts / 86400)
local utcJDN = 2440588 + daysSinceEpoch -- Jan 1 1970 = JDN 2440588
local utcHour = math.mod(math.floor(ts / 3600), 24)
local ok, utc = pcall(date, "!*t")
local utcYear
if ok and utc and utc.year then
utcYear = utc.year
else
local lt = date("*t")
utcYear = lt.year
end
local mar31dow = math.mod(DmfJDN(utcYear, 3, 31), 7)
local lastSunMar = 31 - math.mod(mar31dow + 1, 7)
local oct31dow = math.mod(DmfJDN(utcYear, 10, 31), 7)
local lastSunOct = 31 - math.mod(oct31dow + 1, 7)
local utcMonth = math.floor((daysSinceEpoch - 10957) / 30.44) + 1
if ok and utc and utc.month then utcMonth = utc.month end
local utcDay = 0
if ok and utc then utcDay = utc.day or 0 end
local afterSpring = (utcMonth > 3)
or (utcMonth == 3 and utcDay > lastSunMar)
or (utcMonth == 3 and utcDay == lastSunMar and utcHour >= 1)
local beforeAutumn = (utcMonth < 10)
or (utcMonth == 10 and utcDay < lastSunOct)
or (utcMonth == 10 and utcDay == lastSunOct and utcHour < 1)
local offset = (afterSpring and beforeAutumn) and 2 or 1 -- CEST=2, CET=1
local cetJDN = utcJDN
if utcHour + offset >= 24 then cetJDN = cetJDN + 1 end
return cetJDN
end
local function GetDmfSchedule()
local todayJDN = GetCETJDN()
local dow = math.mod(todayJDN, 7) -- 0=Mon 1=Tue 2=Wed 3=Thu 4=Fri 5=Sat 6=Sun
local isWed = (dow == 2)
-- Week runs Sun(6)-Sat(5); ref Sunday March 8 = Goldshire
if not dmfRefJDN then dmfRefJDN = DmfJDN(2026, 3, 8) end
local daysSinceSun = math.mod(dow + 1, 7) -- Sun=0 Mon=1 Tue=2 Wed=3 Thu=4 Fri=5 Sat=6
local thisSunJDN = todayJDN - daysSinceSun
local weeksDiff = math.floor((thisSunJDN - dmfRefJDN) / 7)
if weeksDiff < 0 then weeksDiff = -weeksDiff end
local ai = math.mod(weeksDiff, 2) == 0 and DMF_REF_LOC or (3 - DMF_REF_LOC)
local daysLeft = 6 - daysSinceSun -- days until Saturday (0 on Sat itself)
local daysUntilNext = daysLeft + 1 -- days until next Sunday
return ai, isWed, daysLeft, daysUntilNext
end
local function CreateDmfPin(i)
if dmfPins[i] then return end
if not WorldMapButton then return end
local f = CreateFrame("Button", "NanamiDMFPin" .. i, WorldMapButton)
f:SetWidth(DMF_SIZE); f:SetHeight(DMF_SIZE)
f:SetFrameStrata("FULLSCREEN_DIALOG")
f:SetFrameLevel(200)
local ico = f:CreateTexture(nil, "OVERLAY")
ico:SetTexture(DMF_ICON)
ico:SetWidth(DMF_SIZE); ico:SetHeight(DMF_SIZE)
ico:SetPoint("CENTER", f, "CENTER", 0, 0)
f.icon = ico
local gl = f:CreateTexture(nil, "OVERLAY")
gl:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
gl:SetBlendMode("ADD")
gl:SetWidth(DMF_SIZE * 2.2); gl:SetHeight(DMF_SIZE * 2.2)
gl:SetPoint("CENTER", f, "CENTER", 0, 0)
local _glc = SFrames.ActiveTheme and SFrames.ActiveTheme.accentLight or { 1, 0.84, 0 }
gl:SetVertexColor(_glc[1], _glc[2], _glc[3], 0.35)
f.glow = gl
f.locIdx = i
f:SetScript("OnEnter", function()
local loc = DMF_LOCATIONS[this.locIdx]
local other = DMF_LOCATIONS[3 - this.locIdx]
local ai, iw, dl, ds = GetDmfSchedule()
local here = (ai == this.locIdx)
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:AddLine("暗月马戏团", 1, 0.84, 0)
GameTooltip:AddLine(loc.fullName, 0.7, 0.7, 0.7)
GameTooltip:AddLine(" ")
if here then
if iw then
GameTooltip:AddLine("今天是马戏团休息日 (周三)", 1, 0.3, 0.3)
GameTooltip:AddLine("马戏团在此处,明天恢复营业", 0.4, 1, 0.4)
GameTooltip:AddLine(" ")
GameTooltip:AddLine("本轮还剩 " .. dl .. " 天 (周六结束)", 0.8, 0.8, 0.8)
else
GameTooltip:AddLine("马戏团正在此处营业中!", 0.4, 1, 0.4)
GameTooltip:AddLine(" ")
if dl > 1 then
GameTooltip:AddLine("剩余 " .. dl .. " 天 (周六结束)", 0.8, 0.8, 0.8)
elseif dl == 1 then
GameTooltip:AddLine("明天是本轮最后一天", 1, 0.84, 0)
else
GameTooltip:AddLine("今天是本轮最后一天!", 1, 0.84, 0)
GameTooltip:AddLine("之后将转移至 " .. other.fullName, 0.6, 0.8, 1)
end
end
else
GameTooltip:AddLine("马戏团目前不在此处", 0.6, 0.6, 0.6)
GameTooltip:AddLine(" ")
GameTooltip:AddLine("当前位置: " .. other.fullName, 0.6, 0.8, 1)
if iw then
GameTooltip:AddLine("今天对方休息," .. ds .. " 天后来此", 1, 0.84, 0)
else
GameTooltip:AddLine(ds .. " 天后到来", 1, 0.84, 0)
end
end
GameTooltip:AddLine(" ")
GameTooltip:AddLine("点击打开Buff指引", 0.5, 0.5, 0.5)
GameTooltip:Show()
end)
f:SetScript("OnLeave", function() GameTooltip:Hide() end)
f:SetScript("OnClick", function()
if SFrames.DarkmoonGuide and SFrames.DarkmoonGuide.Toggle then
SFrames.DarkmoonGuide:Toggle()
end
end)
f:RegisterForClicks("LeftButtonUp")
f:Hide()
dmfPins[i] = f
end
local function UpdateDarkmoonPins()
if not WorldMapButton or not WorldMapFrame:IsVisible() then
for i = 1, 2 do if dmfPins[i] then dmfPins[i]:Hide() end end
return
end
local mapFile = GetMapInfo and GetMapInfo() or ""
local curC = GetCurrentMapContinent and GetCurrentMapContinent() or 0
local curZ = GetCurrentMapZone and GetCurrentMapZone() or 0
local ai, iw = GetDmfSchedule()
local w = WorldMapButton:GetWidth()
local h = WorldMapButton:GetHeight()
if not w or not h or w == 0 or h == 0 then return end
for i = 1, 2 do
CreateDmfPin(i)
local pin = dmfPins[i]
local loc = DMF_LOCATIONS[i]
local px, py
local show = false
local isContinent = false
if curZ > 0 and curC == loc.cont and loc.zoneIdx and curZ == loc.zoneIdx then
show = true
px, py = loc.zx, loc.zy
elseif mapFile ~= "" and mapFile == loc.zone then
show = true
px, py = loc.zx, loc.zy
elseif curZ == 0 and curC == loc.cont then
show = true
isContinent = true
px, py = loc.cx, loc.cy
end
if show then
local sz = isContinent and DMF_SIZE_CONT or DMF_SIZE
pin:SetWidth(sz); pin:SetHeight(sz)
pin.icon:SetWidth(sz); pin.icon:SetHeight(sz)
pin.glow:SetWidth(sz * 2.2); pin.glow:SetHeight(sz * 2.2)
pin:ClearAllPoints()
pin:SetPoint("CENTER", WorldMapButton, "TOPLEFT", w * px, -h * py)
if ai == i then
if iw then
pin.icon:SetVertexColor(0.75, 0.75, 0.75)
pin:SetAlpha(0.85)
pin.glow:Hide()
else
pin.icon:SetVertexColor(1, 1, 1)
pin:SetAlpha(1)
pin.glow:Show()
end
else
pin.icon:SetVertexColor(0.45, 0.45, 0.45)
pin:SetAlpha(0.7)
pin.glow:Hide()
end
pin:Show()
else
pin:Hide()
end
end
dmfPulse = dmfPulse + 0.03
if dmfPulse > 6.2832 then dmfPulse = dmfPulse - 6.2832 end
for i = 1, 2 do
local p = dmfPins[i]
if p and p:IsShown() and p.glow and p.glow:IsShown() then
p.glow:SetAlpha(0.25 + 0.20 * math.sin(dmfPulse * 2))
end
end
end
SLASH_DMFMAP1 = "/dmfmap"
SlashCmdList["DMFMAP"] = function(msg)
local cf = DEFAULT_CHAT_FRAME
if msg == "scan" then
local savedC = GetCurrentMapContinent and GetCurrentMapContinent() or 0
local savedZ = GetCurrentMapZone and GetCurrentMapZone() or 0
for c = 1, 2 do
local cname = c == 1 and "Kalimdor" or "Eastern Kingdoms"
cf:AddMessage("|cffffcc66[DMF Scan] " .. cname .. " (cont=" .. c .. "):|r")
local zones = { GetMapZones(c) }
for idx = 1, table.getn(zones) do
SetMapZoom(c, idx)
local mf = GetMapInfo and GetMapInfo() or "(nil)"
local zname = zones[idx] or "?"
if string.find(string.lower(mf), "elwynn") or string.find(string.lower(zname), "elwynn")
or string.find(string.lower(mf), "mulgore") or string.find(string.lower(zname), "mulgore") then
cf:AddMessage(" |cff00ff00>>|r idx=" .. idx .. " file=" .. tostring(mf) .. " name=" .. tostring(zname))
else
cf:AddMessage(" idx=" .. idx .. " file=" .. tostring(mf) .. " name=" .. tostring(zname))
end
end
end
if savedZ > 0 then SetMapZoom(savedC, savedZ)
elseif savedC > 0 then SetMapZoom(savedC, 0)
else if SetMapToCurrentZone then SetMapToCurrentZone() end end
return
end
local ai, iw, dl, ds = GetDmfSchedule()
local n = DMF_LOCATIONS[ai] and DMF_LOCATIONS[ai].name or "?"
local mf = GetMapInfo and GetMapInfo() or "(nil)"
local curC = GetCurrentMapContinent and GetCurrentMapContinent() or 0
local curZ = GetCurrentMapZone and GetCurrentMapZone() or 0
local cetJDN = GetCETJDN()
local dow = math.mod(cetJDN, 7)
local dayNames = { [0]="Mon", [1]="Tue", [2]="Wed", [3]="Thu", [4]="Fri", [5]="Sat", [6]="Sun" }
local localT = date("%H:%M")
local ts = time()
local utcH = math.mod(math.floor(ts / 3600), 24)
local utcM = math.mod(math.floor(ts / 60), 60)
cf:AddMessage("|cffffcc66[暗月马戏团]|r 本周: " .. n
.. " | CET " .. (dayNames[dow] or "?") .. (iw and "(休息)" or "")
.. " | 剩余" .. dl .. "天 | " .. ds .. "天后轮换")
cf:AddMessage(" 本地=" .. localT .. " UTC=" .. string.format("%02d:%02d", utcH, utcM)
.. " | map=" .. tostring(mf)
.. " | cont=" .. curC .. " zone=" .. curZ)
for i = 1, 2 do
local p = dmfPins[i]
local loc = DMF_LOCATIONS[i]
cf:AddMessage(" " .. loc.name .. ": "
.. (p and ("shown=" .. tostring(p:IsShown())) or "未创建")
.. " | zoneIdx=" .. tostring(loc.zoneIdx or "未发现")
.. " | file=" .. loc.zone .. " cont=" .. loc.cont)
end
end
--------------------------------------------------------------------------------
-- 9. Chat Link Integration: transform <npin:...> and handle nmwp: clicks
--------------------------------------------------------------------------------
local function TransformMapLinks(text)
if not text or type(text) ~= "string" then return text end
if not string.find(text, "<npin:", 1, true) then return text end
local result = string.gsub(text, "<npin:(%d+):(%d+):([%d%.]+):([%d%.]+):([^>]*)>",
function(c, z, x, y, name)
local display = name
if not display or display == "" then display = "地图标记" end
local _wh = (SFrames.Theme and SFrames.Theme:GetAccentHex()) or "ffFFB3D9"
return "|c" .. _wh .. "|Hnmwp:" .. c .. ":" .. z .. ":" .. x .. ":" .. y .. "|h[" .. display .. " (" .. x .. ", " .. y .. ")]|h|r"
end)
return result
end
local function HookChatFrameForMapLinks(cf)
if not cf or not cf.AddMessage or cf._nanamiMapLinkHooked then return end
cf._nanamiMapLinkHooked = true
local orig = cf.AddMessage
cf.AddMessage = function(self, text, r, g, b, alpha, holdTime)
if text and type(text) == "string" then
text = TransformMapLinks(text)
end
orig(self, text, r, g, b, alpha, holdTime)
end
end
local function HookAllChatFramesForMapLinks()
local _G = getfenv(0)
for i = 1, 10 do
local cf = _G["ChatFrame" .. i]
if cf then HookChatFrameForMapLinks(cf) end
end
end
local function HookSetItemRefForWaypoints()
local _G = getfenv(0)
local origSIR = _G.SetItemRef
_G.SetItemRef = function(link, text, button)
if link and string.sub(link, 1, 5) == "nmwp:" then
local data = string.sub(link, 6)
WM:HandleWaypointLink(data)
return
end
if origSIR then
origSIR(link, text, button)
end
end
end
--------------------------------------------------------------------------------
-- 10. WorldMapButton Click Hook for Placing Waypoints
--------------------------------------------------------------------------------
local function HookWorldMapButtonClick()
if not WorldMapButton then return end
local prevOnClick = WorldMapButton:GetScript("OnClick")
WorldMapButton:SetScript("OnClick", function()
if arg1 == "LeftButton" and IsControlKeyDown() then
local mx, my = GetMapCursorPos()
if mx and my then
local c = GetCurrentMapContinent and GetCurrentMapContinent() or 0
local z = GetCurrentMapZone and GetCurrentMapZone() or 0
local name = GetCurrentMapName()
WM:SetWaypoint(c, z, mx, my, name)
end
return
end
if prevOnClick then prevOnClick() end
end)
end
--------------------------------------------------------------------------------
-- 11. Navigation Map (迷你全区域地图 - 类似正式服 Shift+M)
-- Shows the entire zone map scaled down. All state in table N.
--------------------------------------------------------------------------------
local N = {
MW = 1002, MH = 668,
CW = { 256, 256, 256, 234 },
RH = { 256, 256, 156 },
DEF_W = 350, MIN_W = 200, MAX_W = 600,
curMap = "", pulse = 0,
overlays = {}, overlayCount = 0,
}
local NC = {
PBG = _A.panelBg, PBD = _A.panelBorder, TC = _A.title, DT = _A.dimText,
}
local function GetNavConfig()
local cfg = WM:GetConfig()
if type(cfg.nav) ~= "table" then
cfg.nav = { enabled = false, width = N.DEF_W, alpha = 0.70, locked = false }
end
if not cfg.nav.width then cfg.nav.width = N.DEF_W end
if cfg.nav.width < N.MIN_W then cfg.nav.width = N.MIN_W end
if cfg.nav.width > N.MAX_W then cfg.nav.width = N.MAX_W end
return cfg.nav
end
local function SaveNavPos()
if not N.frame then return end
local nc = GetNavConfig()
local pt, _, rp, x, y = N.frame:GetPoint()
nc.aF = pt; nc.aT = rp; nc.pX = x; nc.pY = y
end
local function NavNextPow2(n)
local p = 16
while p < n do p = p * 2 end
return p
end
local function NavApplySize(w)
if not N.frame or not N.tiles then return end
local h = w * N.MH / N.MW
local s = w / N.MW
N.frame:SetWidth(w); N.frame:SetHeight(h)
local idx = 0
for row = 1, 3 do
for col = 1, 4 do
idx = idx + 1
local t = N.tiles[idx]
t:SetWidth(N.CW[col] * s); t:SetHeight(N.RH[row] * s)
local xO, yO = 0, 0
for c = 1, col - 1 do xO = xO + N.CW[c] end
for r = 1, row - 1 do yO = yO + N.RH[r] end
t:ClearAllPoints()
t:SetPoint("TOPLEFT", N.frame, "TOPLEFT", xO * s, -yO * s)
end
end
for i = 1, N.overlayCount do
if N.overlays[i] and N.ovData and N.ovData[i] then
local d = N.ovData[i]
local tex = N.overlays[i]
tex:SetWidth(d.pw * s); tex:SetHeight(d.ph * s)
tex:ClearAllPoints()
tex:SetPoint("TOPLEFT", N.frame, "TOPLEFT", d.ox * s, -d.oy * s)
end
end
end
local function UpdateNavOverlays(mapFile)
if not N.frame or not mapFile or mapFile == "" then return end
local db = MapOverlayData or LibMapOverlayData or zMapOverlayData or mapOverlayData
if not db then
for i = 1, N.overlayCount do if N.overlays[i] then N.overlays[i]:Hide() end end
return
end
local zoneData = db[mapFile]
if not zoneData then
for i = 1, N.overlayCount do if N.overlays[i] then N.overlays[i]:Hide() end end
return
end
local nc = GetNavConfig()
local s = (nc.width or N.DEF_W) / N.MW
local prefix = "Interface\\WorldMap\\" .. mapFile .. "\\"
local texIdx = 0
if not N.ovData then N.ovData = {} end
for idx = 1, table.getn(zoneData) do
local entry = zoneData[idx]
local _, _, oName, sW, sH, sX, sY = string.find(entry, "^(%u+):(%d+):(%d+):(%d+):(%d+)$")
if not oName then
_, _, oName, sW, sH, sX, sY = string.find(entry, "^([^:]+):(%d+):(%d+):(%d+):(%d+)$")
end
if oName then
local tw = tonumber(sW)
local th = tonumber(sH)
local ox = tonumber(sX)
local oy = tonumber(sY)
local tName = prefix .. oName
local numH = math.ceil(tw / 256)
local numV = math.ceil(th / 256)
for row = 1, numV do
local pxH, fileH
if row < numV then
pxH = 256; fileH = 256
else
pxH = math.mod(th, 256)
if pxH == 0 then pxH = 256 end
fileH = NavNextPow2(pxH)
end
for col = 1, numH do
texIdx = texIdx + 1
local pxW, fileW
if col < numH then
pxW = 256; fileW = 256
else
pxW = math.mod(tw, 256)
if pxW == 0 then pxW = 256 end
fileW = NavNextPow2(pxW)
end
if not N.overlays[texIdx] then
N.overlays[texIdx] = N.frame:CreateTexture(nil, "OVERLAY")
end
local tex = N.overlays[texIdx]
local realOx = ox + 256 * (col - 1)
local realOy = oy + 256 * (row - 1)
N.ovData[texIdx] = { pw = pxW, ph = pxH, ox = realOx, oy = realOy }
tex:SetWidth(pxW * s); tex:SetHeight(pxH * s)
tex:SetTexCoord(0, pxW / fileW, 0, pxH / fileH)
tex:ClearAllPoints()
tex:SetPoint("TOPLEFT", N.frame, "TOPLEFT", realOx * s, -realOy * s)
local tileIndex = ((row - 1) * numH) + col
tex:SetTexture(tName .. tileIndex)
tex:SetVertexColor(1, 1, 1, 1)
tex:Show()
end
end
end
end
for i = texIdx + 1, N.overlayCount do
if N.overlays[i] then N.overlays[i]:Hide() end
end
if texIdx > N.overlayCount then N.overlayCount = texIdx end
end
local function CreateNavMap()
if N.frame then return end
local nc = GetNavConfig()
local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
local w = nc.width or N.DEF_W
local h = w * N.MH / N.MW
local s = w / N.MW
local f = CreateFrame("Frame", "NanamiNavMap", UIParent)
N.frame = f
f:SetWidth(w); f:SetHeight(h)
f:SetAlpha(nc.alpha or 0.70)
f:SetMovable(true); f:EnableMouse(true); f:EnableMouseWheel(1)
f:SetClampedToScreen(true)
f:SetFrameStrata("LOW"); f:SetFrameLevel(10)
f:Hide()
f:ClearAllPoints()
if nc.aF and nc.pX and nc.pY then
f:SetPoint(nc.aF, UIParent, nc.aT or nc.aF, nc.pX, nc.pY)
else
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
end
N.tiles = {}
local idx = 0
for row = 1, 3 do
for col = 1, 4 do
idx = idx + 1
local oW = N.CW[col]
local oH = N.RH[row]
local tile = f:CreateTexture("NanamiNavTile" .. idx, "ARTWORK")
tile:SetWidth(oW * s); tile:SetHeight(oH * s)
local xO, yO = 0, 0
for c = 1, col - 1 do xO = xO + N.CW[c] end
for r = 1, row - 1 do yO = yO + N.RH[r] end
tile:SetPoint("TOPLEFT", f, "TOPLEFT", xO * s, -yO * s)
local tcR = (oW >= 256) and 1 or (oW / 256)
local tcB = (oH >= 256) and 1 or (oH / 256)
tile:SetTexCoord(0, tcR, 0, tcB)
N.tiles[idx] = tile
end
end
-- thin border
local borderFrame = CreateFrame("Frame", nil, f)
borderFrame:SetAllPoints(f)
borderFrame:SetFrameLevel(f:GetFrameLevel() + 5)
borderFrame:SetBackdrop({
edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1,
insets = { left = 0, right = 0, top = 0, bottom = 0 },
})
borderFrame:SetBackdropBorderColor(0, 0, 0, 0.50)
-- gradient fade overlay (soft vignette at edges)
local fade = CreateFrame("Frame", nil, f)
fade:SetAllPoints(f)
fade:SetFrameLevel(f:GetFrameLevel() + 6)
local FD = 30
local fadeL = fade:CreateTexture(nil, "OVERLAY")
fadeL:SetTexture("Interface\\Buttons\\WHITE8X8")
fadeL:SetWidth(FD)
fadeL:SetPoint("TOPLEFT", fade, "TOPLEFT", 0, 0)
fadeL:SetPoint("BOTTOMLEFT", fade, "BOTTOMLEFT", 0, 0)
fadeL:SetGradientAlpha("HORIZONTAL", 0,0,0,0.25, 0,0,0,0)
local fadeR = fade:CreateTexture(nil, "OVERLAY")
fadeR:SetTexture("Interface\\Buttons\\WHITE8X8")
fadeR:SetWidth(FD)
fadeR:SetPoint("TOPRIGHT", fade, "TOPRIGHT", 0, 0)
fadeR:SetPoint("BOTTOMRIGHT", fade, "BOTTOMRIGHT", 0, 0)
fadeR:SetGradientAlpha("HORIZONTAL", 0,0,0,0, 0,0,0,0.25)
local fadeT = fade:CreateTexture(nil, "OVERLAY")
fadeT:SetTexture("Interface\\Buttons\\WHITE8X8")
fadeT:SetHeight(FD)
fadeT:SetPoint("TOPLEFT", fade, "TOPLEFT", 0, 0)
fadeT:SetPoint("TOPRIGHT", fade, "TOPRIGHT", 0, 0)
fadeT:SetGradientAlpha("VERTICAL", 0,0,0,0, 0,0,0,0.25)
local fadeB = fade:CreateTexture(nil, "OVERLAY")
fadeB:SetTexture("Interface\\Buttons\\WHITE8X8")
fadeB:SetHeight(FD)
fadeB:SetPoint("BOTTOMLEFT", fade, "BOTTOMLEFT", 0, 0)
fadeB:SetPoint("BOTTOMRIGHT", fade, "BOTTOMRIGHT", 0, 0)
fadeB:SetGradientAlpha("VERTICAL", 0,0,0,0.25, 0,0,0,0)
-- player indicator (arrow + ripple)
local dotFrame = CreateFrame("Frame", nil, f)
N.dotFrame = dotFrame
dotFrame:SetWidth(1); dotFrame:SetHeight(1)
dotFrame:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
dotFrame:SetFrameLevel(f:GetFrameLevel() + 7)
local CIRCLE = "Interface\\Minimap\\UI-Minimap-Background"
N.ripples = {}
for i = 1, 3 do
local rip = dotFrame:CreateTexture(nil, "ARTWORK")
rip:SetTexture(CIRCLE)
rip:SetWidth(8); rip:SetHeight(8)
rip:SetPoint("CENTER", dotFrame, "CENTER", 0, 0)
rip:SetVertexColor(0.0, 0.85, 1.0, 0.35)
rip:SetBlendMode("ADD")
N.ripples[i] = rip
end
local CLASS_ICON = "Interface\\AddOns\\Nanami-UI\\img\\UI-Classes-Circles"
N.dotTex = dotFrame:CreateTexture(nil, "OVERLAY")
N.dotTex:SetTexture(CLASS_ICON)
N.dotTex:SetWidth(14); N.dotTex:SetHeight(14)
N.dotTex:SetPoint("CENTER", dotFrame, "CENTER", 0, 0)
local _, pClass = UnitClass("player")
if pClass and SFrames.CLASS_ICON_TCOORDS and SFrames.CLASS_ICON_TCOORDS[pClass] then
local tc = SFrames.CLASS_ICON_TCOORDS[pClass]
N.dotTex:SetTexCoord(tc[1], tc[2], tc[3], tc[4])
end
N.dirTip = dotFrame:CreateTexture(nil, "OVERLAY")
N.dirTip:SetTexture(CIRCLE)
N.dirTip:SetWidth(4); N.dirTip:SetHeight(4)
N.dirTip:SetPoint("CENTER", dotFrame, "CENTER", 0, 10)
N.dirTip:SetVertexColor(1, 1, 1, 0.90)
N.dirTail = dotFrame:CreateTexture(nil, "OVERLAY")
N.dirTail:SetTexture(CIRCLE)
N.dirTail:SetWidth(2); N.dirTail:SetHeight(2)
N.dirTail:SetPoint("CENTER", dotFrame, "CENTER", 0, -7)
N.dirTail:SetVertexColor(0.6, 0.6, 0.6, 0.50)
-- info bar (zone + coords below map)
local infoFrame = CreateFrame("Frame", nil, f)
infoFrame:SetWidth(w); infoFrame:SetHeight(24)
infoFrame:SetPoint("TOP", f, "BOTTOM", 0, 0)
infoFrame:SetFrameLevel(f:GetFrameLevel() + 6)
N.zoneFS = infoFrame:CreateFontString(nil, "OVERLAY")
N.zoneFS:SetFont(font, 9, "OUTLINE")
N.zoneFS:SetPoint("LEFT", infoFrame, "LEFT", 4, 0)
N.zoneFS:SetTextColor(NC.TC[1], NC.TC[2], NC.TC[3])
N.coordFS = infoFrame:CreateFontString(nil, "OVERLAY")
N.coordFS:SetFont(font, 9, "OUTLINE")
N.coordFS:SetPoint("RIGHT", infoFrame, "RIGHT", -4, 0)
N.coordFS:SetTextColor(0.85, 0.85, 0.85)
-- close button
local cbtn = CreateFrame("Button", nil, f)
cbtn:SetWidth(14); cbtn:SetHeight(14)
cbtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -3, -3)
cbtn:SetFrameLevel(f:GetFrameLevel() + 8)
local cbFS = cbtn:CreateFontString(nil, "OVERLAY")
cbFS:SetFont(font, 11, "OUTLINE"); cbFS:SetPoint("CENTER", 0, 0)
cbFS:SetText("x"); cbFS:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3])
cbtn:SetScript("OnClick", function() WM:ToggleNav() end)
cbtn:SetScript("OnEnter", function() cbFS:SetTextColor(1, 0.3, 0.3) end)
cbtn:SetScript("OnLeave", function() cbFS:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3]) end)
-- hover toolbar (shown on mouse enter)
local toolbar = CreateFrame("Frame", nil, f)
toolbar:SetWidth(w); toolbar:SetHeight(22)
toolbar:SetPoint("BOTTOM", f, "TOP", 0, 2)
toolbar:SetFrameLevel(f:GetFrameLevel() + 9)
toolbar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
toolbar:SetBackdropColor(_A.headerBg[1], _A.headerBg[2], _A.headerBg[3], 0.85)
toolbar:SetBackdropBorderColor(0, 0, 0, 0.50)
toolbar:EnableMouse(true)
toolbar:Hide()
N.toolbar = toolbar
local tipFS = toolbar:CreateFontString(nil, "OVERLAY")
tipFS:SetFont(font, 9, "OUTLINE")
tipFS:SetPoint("CENTER", toolbar, "CENTER", 0, 0)
tipFS:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3])
tipFS:SetText("拖动移位 | 滚轮缩放 | Ctrl+滚轮透明度")
local lockBtn = CreateFrame("Button", nil, toolbar)
lockBtn:SetWidth(28); lockBtn:SetHeight(16)
lockBtn:SetPoint("RIGHT", toolbar, "RIGHT", -4, 0)
lockBtn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
lockBtn:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], 0.90)
lockBtn:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.6)
local lockFS = lockBtn:CreateFontString(nil, "OVERLAY")
lockFS:SetFont(font, 8, "OUTLINE"); lockFS:SetPoint("CENTER", 0, 0)
N.lockFS = lockFS
local function UpdateLockText()
local lk = GetNavConfig().locked
if lk then
lockFS:SetText("解锁"); lockFS:SetTextColor(1.0, 0.5, 0.5)
else
lockFS:SetText("锁定"); lockFS:SetTextColor(0.5, 1.0, 0.5)
end
end
UpdateLockText()
lockBtn:SetScript("OnClick", function()
local cfg = GetNavConfig()
cfg.locked = not cfg.locked
UpdateLockText()
end)
lockBtn:SetScript("OnEnter", function()
this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4])
end)
lockBtn:SetScript("OnLeave", function()
this:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.6)
end)
N.hideTimer = 0
f:SetScript("OnEnter", function()
N.hideTimer = 0
if N.toolbar then N.toolbar:Show() end
if cbtn then cbtn:Show() end
end)
f:SetScript("OnLeave", function()
N.hideTimer = 1.5
end)
toolbar:SetScript("OnEnter", function()
N.hideTimer = 0
end)
toolbar:SetScript("OnLeave", function()
N.hideTimer = 1.5
end)
cbtn:Hide()
-- dragging
f:SetScript("OnMouseDown", function()
if arg1 == "LeftButton" and not GetNavConfig().locked then this:StartMoving() end
end)
f:SetScript("OnMouseUp", function() this:StopMovingOrSizing(); SaveNavPos() end)
-- scroll: resize from center / alpha
f:SetScript("OnMouseWheel", function()
local cfg = GetNavConfig()
if IsControlKeyDown() then
local a = (cfg.alpha or this:GetAlpha()) + arg1 * 0.10
if a < 0.15 then a = 0.15 end; if a > 1 then a = 1 end
cfg.alpha = a; this:SetAlpha(a)
else
local oldW = this:GetWidth()
local oldH = this:GetHeight()
local nw = cfg.width + arg1 * 25
if nw < N.MIN_W then nw = N.MIN_W end
if nw > N.MAX_W then nw = N.MAX_W end
local nh = nw * N.MH / N.MW
local dw = (nw - oldW) / 2
local dh = (nh - oldH) / 2
local pt, rel, rp, px, py = this:GetPoint()
if pt and px and py then
this:ClearAllPoints()
this:SetPoint(pt, rel, rp, px - dw, py + dh)
end
cfg.width = nw
NavApplySize(nw)
if N.toolbar then N.toolbar:SetWidth(nw) end
SaveNavPos()
end
end)
end
local function UpdateNavMap()
if not N.frame or not N.tiles or not N.frame:IsVisible() then return end
if WorldMapFrame and WorldMapFrame:IsVisible() then return end
if SetMapToCurrentZone then SetMapToCurrentZone() end
local mapFile = GetMapInfo and GetMapInfo() or ""
if mapFile ~= "" and mapFile ~= N.curMap then
N.curMap = mapFile
for i = 1, 12 do
N.tiles[i]:SetTexture("Interface\\WorldMap\\" .. mapFile .. "\\" .. mapFile .. i)
end
UpdateNavOverlays(mapFile)
end
if mapFile ~= "" then
local px, py = GetPlayerMapPosition("player")
if px and py and (px > 0 or py > 0) then
local fw = N.frame:GetWidth()
local fh = N.frame:GetHeight()
if N.dotFrame then
N.dotFrame:ClearAllPoints()
N.dotFrame:SetPoint("TOPLEFT", N.frame, "TOPLEFT", px * fw, -py * fh)
end
if GetPlayerFacing and N.dirTip then
local facing = GetPlayerFacing()
if facing then
local r = 10
local tipX = -math.sin(facing) * r
local tipY = math.cos(facing) * r
N.dirTip:ClearAllPoints()
N.dirTip:SetPoint("CENTER", N.dotFrame, "CENTER", tipX, tipY)
if N.dirTail then
local tr = 7
N.dirTail:ClearAllPoints()
N.dirTail:SetPoint("CENTER", N.dotFrame, "CENTER", -tipX / r * tr, -tipY / r * tr)
end
end
end
local coordStr = string.format("%.1f, %.1f", px * 100, py * 100)
if N.coordFS then N.coordFS:SetText(coordStr) end
end
end
local zn = GetZoneText and GetZoneText() or ""
if zn ~= "" and N.zoneFS then N.zoneFS:SetText(zn) end
end
local function UpdateNavPulse(dt)
if not N.frame or not N.ripples or not N.frame:IsVisible() then return end
N.pulse = (N.pulse or 0) + 0.05
if N.pulse > 6.2832 then N.pulse = N.pulse - 6.2832 end
local PHASE_OFF = 2.0944
for i = 1, 3 do
local rip = N.ripples[i]
if rip then
local t = math.mod(N.pulse + (i - 1) * PHASE_OFF, 6.2832) / 6.2832
local sz = 6 + 28 * t
local a = 0.40 * (1 - t)
rip:SetWidth(sz); rip:SetHeight(sz)
rip:SetVertexColor(0.0, 0.85, 1.0, a)
end
end
if N.hideTimer and N.hideTimer > 0 then
N.hideTimer = N.hideTimer - (dt or 0.04)
if N.hideTimer <= 0 then
N.hideTimer = 0
if N.toolbar then N.toolbar:Hide() end
end
end
end
function WM:ToggleNav()
local nc = GetNavConfig()
nc.enabled = not nc.enabled
CreateNavMap()
if nc.enabled then
N.frame:Show(); N.curMap = ""
UpdateNavMap()
if SFrames and SFrames.Print then
SFrames:Print("导航地图: |cff00ff00已开启|r (滚轮缩放 | Ctrl+滚轮透明度 | 拖动)")
end
else
N.frame:Hide()
if SFrames and SFrames.Print then
SFrames:Print("导航地图: |cffff0000已关闭|r")
end
end
end
function WM:ShowNav()
local nc = GetNavConfig()
CreateNavMap()
if not nc.enabled then nc.enabled = true end
if WorldMapFrame and WorldMapFrame:IsVisible() then
WorldMapFrame:Hide()
end
N.frame:Show(); N.curMap = ""
UpdateNavMap()
end
local function CreateNavToggleBtn()
if not skinFrame or not coordOverlay then return end
if N.toggleBtn then return end
local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
local b = CreateFrame("Button", nil, coordOverlay)
N.toggleBtn = b
b:SetWidth(70); b:SetHeight(20)
b:SetPoint("BOTTOMRIGHT", skinFrame, "BOTTOMRIGHT", -12, 22)
b:SetFrameLevel(coordOverlay:GetFrameLevel() + 1)
b:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
b:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
b:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
local fs = b:CreateFontString(nil, "OVERLAY")
fs:SetFont(font, 10, "OUTLINE"); fs:SetPoint("CENTER", 0, 0)
fs:SetText("缩略地图"); fs:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3])
b:SetScript("OnClick", function() WM:ShowNav() end)
b:SetScript("OnEnter", function()
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4])
end)
b:SetScript("OnLeave", function()
this:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
this:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
end)
end
local function InitNavMap()
CreateNavMap()
CreateNavToggleBtn()
local nc = GetNavConfig()
if nc.enabled then
N.frame:Show(); N.curMap = ""
end
local mapEl, animEl = 0, 0
local u = CreateFrame("Frame", nil, UIParent)
u:SetScript("OnUpdate", function()
local dt = arg1 or 0
mapEl = mapEl + dt
animEl = animEl + dt
if mapEl >= 0.10 then mapEl = 0; UpdateNavMap() end
if animEl >= 0.04 then
local elapsed = animEl; animEl = 0; UpdateNavPulse(elapsed)
end
end)
end
--------------------------------------------------------------------------------
-- 12. Initialize
--------------------------------------------------------------------------------
function WM:Initialize()
local cfg = self:GetConfig()
if not cfg.enabled then return end
HideBlizzardDecorations()
CreateNanamiSkin()
CreateTooltipBG()
self:SetupWindowMode()
SkinWorldMapControls()
CreateWaypointPin()
HookWorldMapButtonClick()
HookTooltipShow(WorldMapTooltip)
HookTooltipShow(GameTooltip)
HookAllChatFramesForMapLinks()
HookSetItemRefForWaypoints()
local origUpdater = OnFrameUpdate
local function NewOnFrameUpdate()
origUpdater()
UpdatePinPosition()
UpdateDarkmoonPins()
end
local updater = CreateFrame("Frame", nil, UIParent)
updater:SetScript("OnUpdate", NewOnFrameUpdate)
InitNavMap()
DiscoverDmfZoneIndices()
self.initialized = true
end