Files
Nanami-UI/Minimap.lua
2026-03-16 13:48:46 +08:00

591 lines
19 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--------------------------------------------------------------------------------
-- Nanami-UI: Minimap Skin (Minimap.lua)
-- Custom cat-paw pixel art frame for the minimap
--------------------------------------------------------------------------------
SFrames.Minimap = SFrames.Minimap or {}
local MM = SFrames.Minimap
local _A = SFrames.ActiveTheme
local MAP_STYLES = {
{ key = "map", tex = "Interface\\AddOns\\Nanami-UI\\img\\map", label = "猫爪", plateY = -10, textColor = {0.45, 0.32, 0.20} },
{ key = "zs", tex = "Interface\\AddOns\\Nanami-UI\\img\\zs", label = "战士", plateY = -6, textColor = {1, 1, 1} },
{ key = "qs", tex = "Interface\\AddOns\\Nanami-UI\\img\\qs", label = "圣骑士", plateY = -6, textColor = {0.22, 0.13, 0.07} },
{ key = "lr", tex = "Interface\\AddOns\\Nanami-UI\\img\\lr", label = "猎人", plateY = -6, textColor = {1, 1, 1} },
{ key = "qxz", tex = "Interface\\AddOns\\Nanami-UI\\img\\qxz", label = "潜行者", plateY = -6, textColor = {1, 1, 1} },
{ key = "ms", tex = "Interface\\AddOns\\Nanami-UI\\img\\ms", label = "牧师", plateY = -6, textColor = {0.22, 0.13, 0.07} },
{ key = "sm", tex = "Interface\\AddOns\\Nanami-UI\\img\\sm", label = "萨满", plateY = -6, textColor = {1, 1, 1} },
{ key = "fs", tex = "Interface\\AddOns\\Nanami-UI\\img\\fs", label = "法师", plateY = -6, textColor = {1, 1, 1} },
{ key = "ss", tex = "Interface\\AddOns\\Nanami-UI\\img\\ss", label = "术士", plateY = -6, textColor = {1, 1, 1} },
{ key = "dly", tex = "Interface\\AddOns\\Nanami-UI\\img\\dly", label = "德鲁伊", plateY = -6, textColor = {0.22, 0.13, 0.07} },
}
local TEX_SIZE = 512
local CIRCLE_CX = 256
local CIRCLE_CY = 256
local CIRCLE_R = 210
local PLATE_X = 103
local PLATE_Y = 29
local PLATE_W = 200
local PLATE_H = 66
local BASE_SIZE = 180
local CLASS_STYLE_MAP = {
["Warrior"] = "zs", ["WARRIOR"] = "zs", ["战士"] = "zs",
["Paladin"] = "qs", ["PALADIN"] = "qs", ["圣骑士"] = "qs",
["Hunter"] = "lr", ["HUNTER"] = "lr", ["猎人"] = "lr",
["Rogue"] = "qxz", ["ROGUE"] = "qxz", ["潜行者"] = "qxz",
["Priest"] = "ms", ["PRIEST"] = "ms", ["牧师"] = "ms",
["Shaman"] = "sm", ["SHAMAN"] = "sm", ["萨满祭司"] = "sm",
["Mage"] = "fs", ["MAGE"] = "fs", ["法师"] = "fs",
["Warlock"] = "ss", ["WARLOCK"] = "ss", ["术士"] = "ss",
["Druid"] = "dly", ["DRUID"] = "dly", ["德鲁伊"] = "dly",
}
local DEFAULTS = {
enabled = true,
scale = 1.0,
showClock = true,
showCoords = true,
mapStyle = "auto",
posX = -5,
posY = -5,
mailIconX = nil,
mailIconY = nil,
}
local container, overlayFrame, overlayTex
local zoneFs, clockFs, clockBg, coordFs
local built = false
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetDB()
if not SFramesDB then SFramesDB = {} end
if type(SFramesDB.Minimap) ~= "table" then SFramesDB.Minimap = {} end
local db = SFramesDB.Minimap
for k, v in pairs(DEFAULTS) do
if db[k] == nil then db[k] = v end
end
return db
end
local function ResolveStyleKey()
local key = GetDB().mapStyle or "auto"
if key == "auto" then
local localName, engName = UnitClass("player")
key = CLASS_STYLE_MAP[engName]
or CLASS_STYLE_MAP[localName]
or "map"
end
return key
end
local function GetCurrentStyle()
local key = ResolveStyleKey()
for _, s in ipairs(MAP_STYLES) do
if s.key == key then return s end
end
return MAP_STYLES[1]
end
local function GetMapTexture()
return GetCurrentStyle().tex
end
local function S(texPx, frameSize)
return texPx / TEX_SIZE * frameSize
end
local function FrameSize()
return math.floor(BASE_SIZE * ((GetDB().scale) or 1))
end
local function ApplyPosition()
if not container then return end
local db = GetDB()
local x = tonumber(db.posX) or -5
local y = tonumber(db.posY) or -5
container:ClearAllPoints()
container:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", x, y)
end
--------------------------------------------------------------------------------
-- Hide default Blizzard minimap chrome
-- MUST be called AFTER BuildFrame (Minimap is already reparented)
--------------------------------------------------------------------------------
local function HideDefaultElements()
local kill = {
MinimapBorder,
MinimapBorderTop,
MinimapZoomIn,
MinimapZoomOut,
MinimapToggleButton,
MiniMapWorldMapButton,
GameTimeFrame,
MinimapZoneTextButton,
MiniMapTracking,
MinimapBackdrop,
}
for _, f in ipairs(kill) do
if f then
f:Hide()
f.Show = function() end
end
end
-- Hide all tracking-related frames (Turtle WoW dual tracking, etc.)
local trackNames = {
"MiniMapTrackingButton", "MiniMapTrackingFrame",
"MiniMapTrackingIcon", "MiniMapTracking1", "MiniMapTracking2",
}
for _, name in ipairs(trackNames) do
local f = _G[name]
if f and f.Hide then
f:Hide()
f.Show = function() end
end
end
-- Also hide any tracking textures that are children of Minimap
if Minimap.GetChildren then
local children = { Minimap:GetChildren() }
for _, child in ipairs(children) do
local n = child.GetName and child:GetName()
if n and string.find(n, "Track") then
child:Hide()
child.Show = function() end
end
end
end
-- Move MinimapCluster off-screen instead of Hide()
-- Hide() would cascade-hide children that were still parented at load time
if MinimapCluster then
MinimapCluster:ClearAllPoints()
MinimapCluster:SetPoint("TOP", UIParent, "BOTTOM", 0, -500)
MinimapCluster:SetWidth(1)
MinimapCluster:SetHeight(1)
MinimapCluster:EnableMouse(false)
end
end
--------------------------------------------------------------------------------
-- Build
--------------------------------------------------------------------------------
local function BuildFrame()
if built then return end
built = true
local fs = FrameSize()
local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs))
-- Main container
container = CreateFrame("Frame", "SFramesMinimapContainer", UIParent)
container:SetWidth(fs)
container:SetHeight(fs)
container:SetFrameStrata("LOW")
container:SetFrameLevel(1)
container:EnableMouse(false)
container:SetClampedToScreen(true)
-- Reparent the actual minimap into our container
Minimap:SetParent(container)
Minimap:ClearAllPoints()
Minimap:SetPoint("CENTER", container, "TOPLEFT",
S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs))
Minimap:SetWidth(mapDiam)
Minimap:SetHeight(mapDiam)
Minimap:SetFrameStrata("LOW")
Minimap:SetFrameLevel(2)
Minimap:Show()
if Minimap.SetMaskTexture then
Minimap:SetMaskTexture("Textures\\MinimapMask")
end
-- Decorative overlay (map.tga with transparent circle)
overlayFrame = CreateFrame("Frame", nil, container)
overlayFrame:SetAllPoints(container)
overlayFrame:SetFrameStrata("LOW")
overlayFrame:SetFrameLevel(Minimap:GetFrameLevel() + 3)
overlayFrame:EnableMouse(false)
overlayTex = overlayFrame:CreateTexture(nil, "ARTWORK")
overlayTex:SetTexture(GetMapTexture())
overlayTex:SetAllPoints(overlayFrame)
-- Zone name on the scroll plate (horizontally centered on frame)
local style = GetCurrentStyle()
local pcy = S(PLATE_Y + PLATE_H / 2 + (style.plateY or -6), fs)
zoneFs = overlayFrame:CreateFontString(nil, "OVERLAY")
zoneFs:SetFont(SFrames:GetFont(), 11, "")
zoneFs:SetPoint("CENTER", container, "TOP", 0, -pcy)
zoneFs:SetWidth(S(PLATE_W + 60, fs))
zoneFs:SetHeight(S(PLATE_H, fs))
zoneFs:SetJustifyH("CENTER")
zoneFs:SetJustifyV("MIDDLE")
local tc = style.textColor or {0.22, 0.13, 0.07}
zoneFs:SetTextColor(tc[1], tc[2], tc[3])
-- Clock background (semi-transparent rounded)
clockBg = CreateFrame("Frame", nil, overlayFrame)
clockBg:SetFrameLevel(overlayFrame:GetFrameLevel() + 1)
clockBg:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 10,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
local _clkBg = _A.clockBg or { 0, 0, 0, 0.55 }
local _clkBd = _A.clockBorder or { 0, 0, 0, 0.3 }
clockBg:SetBackdropColor(_clkBg[1], _clkBg[2], _clkBg[3], _clkBg[4])
clockBg:SetBackdropBorderColor(_clkBd[1], _clkBd[2], _clkBd[3], _clkBd[4])
clockBg:SetWidth(46)
clockBg:SetHeight(18)
clockBg:SetPoint("CENTER", container, "BOTTOM", 0,
S((TEX_SIZE - CIRCLE_CY - CIRCLE_R) / 2, fs))
-- Clock text
clockFs = clockBg:CreateFontString(nil, "OVERLAY")
clockFs:SetFont(SFrames:GetFont(), 10, "OUTLINE")
clockFs:SetPoint("CENTER", clockBg, "CENTER", 0, 0)
clockFs:SetJustifyH("CENTER")
local _clkTxt = _A.clockText or { 0.92, 0.84, 0.72 }
clockFs:SetTextColor(_clkTxt[1], _clkTxt[2], _clkTxt[3])
-- Coordinates (inside circle, near bottom)
coordFs = overlayFrame:CreateFontString(nil, "OVERLAY")
coordFs:SetFont(SFrames:GetFont(), 9, "OUTLINE")
coordFs:SetPoint("BOTTOM", Minimap, "BOTTOM", 0, 8)
coordFs:SetJustifyH("CENTER")
local _coordTxt = _A.coordText or { 0.80, 0.80, 0.80 }
coordFs:SetTextColor(_coordTxt[1], _coordTxt[2], _coordTxt[3])
MM.container = container
MM.overlayFrame = overlayFrame
end
--------------------------------------------------------------------------------
-- Interactions
--------------------------------------------------------------------------------
local function SetupScrollZoom()
Minimap:EnableMouseWheel(true)
Minimap:SetScript("OnMouseWheel", function()
if arg1 > 0 then
Minimap_ZoomIn()
else
Minimap_ZoomOut()
end
end)
end
local function SetupMouseHandler()
Minimap:SetScript("OnMouseUp", function()
if arg1 == "RightButton" then
if MiniMapTrackingDropDown then
ToggleDropDownMenu(1, nil, MiniMapTrackingDropDown, "cursor")
end
else
if Minimap_OnClick then
Minimap_OnClick(this)
end
end
end)
end
MM.ApplyPosition = ApplyPosition
--------------------------------------------------------------------------------
-- Reposition child icons
--------------------------------------------------------------------------------
local function SetupMailDragging()
if not MiniMapMailFrame then return end
MiniMapMailFrame:SetMovable(true)
MiniMapMailFrame:EnableMouse(true)
MiniMapMailFrame:RegisterForDrag("LeftButton")
MiniMapMailFrame:SetScript("OnDragStart", function()
MiniMapMailFrame:StartMoving()
end)
MiniMapMailFrame:SetScript("OnDragStop", function()
MiniMapMailFrame:StopMovingOrSizing()
local db = GetDB()
local cx, cy = MiniMapMailFrame:GetCenter()
local ux, uy = UIParent:GetCenter()
if cx and cy and ux and uy then
db.mailIconX = cx - ux
db.mailIconY = cy - uy
end
end)
end
local function RepositionIcons()
if MiniMapMailFrame then
local db = GetDB()
MiniMapMailFrame:ClearAllPoints()
if db.mailIconX and db.mailIconY then
MiniMapMailFrame:SetPoint("CENTER", UIParent, "CENTER", db.mailIconX, db.mailIconY)
else
local mapDiam = Minimap:GetWidth()
MiniMapMailFrame:SetPoint("RIGHT", Minimap, "RIGHT", 8, mapDiam * 0.12)
end
MiniMapMailFrame:SetFrameStrata("LOW")
MiniMapMailFrame:SetFrameLevel((overlayFrame and overlayFrame:GetFrameLevel() or 5) + 2)
SetupMailDragging()
end
if MiniMapBattlefieldFrame then
MiniMapBattlefieldFrame:ClearAllPoints()
MiniMapBattlefieldFrame:SetPoint("BOTTOMLEFT", Minimap, "BOTTOM", 15, -8)
MiniMapBattlefieldFrame:SetFrameStrata("LOW")
MiniMapBattlefieldFrame:SetFrameLevel((overlayFrame and overlayFrame:GetFrameLevel() or 5) + 2)
end
end
--------------------------------------------------------------------------------
-- Update helpers
--------------------------------------------------------------------------------
local function UpdateZoneText()
if not zoneFs then return end
local text = GetMinimapZoneText and GetMinimapZoneText() or ""
lastZoneText = text
zoneFs:SetText(text)
end
local function SetZoneMap()
if SetMapToCurrentZone then
pcall(SetMapToCurrentZone)
end
end
local clockTimer = 0
local coordTimer = 0
local zoneTimer = 0
local lastZoneText = ""
local function OnUpdate()
local dt = arg1 or 0
local db = GetDB()
-- Clock (every 1 s)
clockTimer = clockTimer + dt
if clockTimer >= 1 then
clockTimer = 0
if db.showClock and clockFs then
local timeStr
if date then
timeStr = date("%H:%M")
else
local h, m = GetGameTime()
timeStr = string.format("%02d:%02d", h, m)
end
clockFs:SetText(timeStr)
clockFs:Show()
if clockBg then clockBg:Show() end
elseif clockFs then
clockFs:Hide()
if clockBg then clockBg:Hide() end
end
end
-- Zone text (every 1 s) catches late API updates missed by events
zoneTimer = zoneTimer + dt
if zoneTimer >= 1 then
zoneTimer = 0
if zoneFs and GetMinimapZoneText then
local cur = GetMinimapZoneText() or ""
if cur ~= lastZoneText then
lastZoneText = cur
zoneFs:SetText(cur)
end
end
end
-- Coordinates (every 0.25 s)
coordTimer = coordTimer + dt
if coordTimer >= 0.25 then
coordTimer = 0
if db.showCoords and coordFs and GetPlayerMapPosition then
local ok, x, y = pcall(GetPlayerMapPosition, "player")
if ok and x and y and (x > 0 or y > 0) then
coordFs:SetText(string.format("%.1f, %.1f", x * 100, y * 100))
coordFs:Show()
else
coordFs:Hide()
end
elseif coordFs then
coordFs:Hide()
end
end
end
--------------------------------------------------------------------------------
-- Public API
--------------------------------------------------------------------------------
function MM:Refresh()
if not container then return end
local fs = FrameSize()
local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs))
container:SetWidth(fs)
container:SetHeight(fs)
Minimap:ClearAllPoints()
Minimap:SetPoint("CENTER", container, "TOPLEFT",
S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs))
Minimap:SetWidth(mapDiam)
Minimap:SetHeight(mapDiam)
if zoneFs then
local style = GetCurrentStyle()
local pcy = S(PLATE_Y + PLATE_H / 2 + (style.plateY or -6), fs)
zoneFs:ClearAllPoints()
zoneFs:SetPoint("CENTER", container, "TOP", 0, -pcy)
zoneFs:SetWidth(S(PLATE_W + 60, fs))
local tc = style.textColor or {0.22, 0.13, 0.07}
zoneFs:SetTextColor(tc[1], tc[2], tc[3])
end
if clockBg then
clockBg:ClearAllPoints()
clockBg:SetPoint("CENTER", container, "BOTTOM", 0,
S((TEX_SIZE - CIRCLE_CY - CIRCLE_R) / 2, fs))
end
if coordFs then
coordFs:ClearAllPoints()
coordFs:SetPoint("BOTTOM", Minimap, "BOTTOM", 0, 8)
end
if overlayTex then
overlayTex:SetTexture(GetMapTexture())
end
UpdateZoneText()
RepositionIcons()
end
MM.MAP_STYLES = MAP_STYLES
--------------------------------------------------------------------------------
-- Shield: re-apply our skin after other addons (ShaguTweaks etc.) touch Minimap
--------------------------------------------------------------------------------
local shielded = false
local function ShieldMinimap()
if shielded then return end
shielded = true
-- Override any external changes to Minimap parent / position / size
if Minimap:GetParent() ~= container then
Minimap:SetParent(container)
end
local fs = FrameSize()
local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs))
Minimap:ClearAllPoints()
Minimap:SetPoint("CENTER", container, "TOPLEFT",
S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs))
Minimap:SetWidth(mapDiam)
Minimap:SetHeight(mapDiam)
Minimap:SetFrameStrata("LOW")
Minimap:SetFrameLevel(2)
Minimap:Show()
if Minimap.SetMaskTexture then
Minimap:SetMaskTexture("Textures\\MinimapMask")
end
HideDefaultElements()
-- Kill any border/backdrop that ShaguTweaks may have injected
local regions = { Minimap:GetRegions() }
for _, r in ipairs(regions) do
if r and r:IsObjectType("Texture") then
local tex = r.GetTexture and r:GetTexture()
if tex and type(tex) == "string" then
local low = string.lower(tex)
if string.find(low, "border") or string.find(low, "backdrop")
or string.find(low, "overlay") or string.find(low, "background") then
r:Hide()
end
end
end
end
shielded = false
end
function MM:Initialize()
if not Minimap then return end
local db = GetDB()
if db.enabled == false then return end
local ok, err = pcall(function()
-- Build first (reparents Minimap), THEN hide old chrome
BuildFrame()
HideDefaultElements()
-- Ensure Minimap is visible after reparent
Minimap:Show()
SetupScrollZoom()
SetupMouseHandler()
-- Apply position from settings
ApplyPosition()
RepositionIcons()
-- Zone text events
SFrames:RegisterEvent("ZONE_CHANGED", function()
SetZoneMap()
UpdateZoneText()
end)
SFrames:RegisterEvent("ZONE_CHANGED_NEW_AREA", function()
SetZoneMap()
UpdateZoneText()
end)
SFrames:RegisterEvent("ZONE_CHANGED_INDOORS", function()
SetZoneMap()
UpdateZoneText()
end)
-- Re-apply after other addons finish loading (ShaguTweaks etc.)
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function()
pcall(ShieldMinimap)
pcall(UpdateZoneText)
end)
-- Delayed zone text: GetMinimapZoneText() may be empty at PLAYER_LOGIN
local zoneDelay = CreateFrame("Frame")
local elapsed = 0
zoneDelay:SetScript("OnUpdate", function()
elapsed = elapsed + (arg1 or 0)
if elapsed >= 1 then
zoneDelay:SetScript("OnUpdate", nil)
pcall(SetZoneMap)
pcall(UpdateZoneText)
end
end)
-- Tick
container:SetScript("OnUpdate", OnUpdate)
-- First refresh
SetZoneMap()
UpdateZoneText()
MM:Refresh()
end)
if not ok then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI Minimap error: " .. tostring(err) .. "|r")
end
end