------------------------------## Author: zRyne ## Version: 1.0
-- Config
------------------------------
local cfg = zMPlusConfig or {}

----------------------------------------
-- 全局字体和描边
----------------------------------------
cfg.GlobalFont = GameFontNormal:GetFont()
cfg.GlobalFontOutline = "OUTLINE"

-------------------------------------------------
-- 次数字体显示设置
-------------------------------------------------
cfg.Weekly = {
    fontSize = 16,

    anchor = {
        point = "TOPLEFT",
        relativeTo = "PVEFrame",
        relativePoint = "TOPRIGHT",
        x = 21,
        y = 0,
    },
	
	--功能开关
    enabled = true,                      -- 总开关
    showWeeklyDetails = true,       -- 显示“完成次数”及详细列表
    useModifierKey = true,            -- 按住 Alt/Shift/Ctrl 切换显示赛季记录
}
----------------------------------------
-- 分数字体显示位置
----------------------------------------
cfg.OverlayText = {
    name  = { x=0, y=22,  size=18 }, -- 副本名
    level = { x=0, y=-22, size=20 }, -- 层数
    score = { x=0, y=0,   size=20 }, -- 分数
}
----------------------------------------
-- 分数文本显示开关
----------------------------------------
cfg.ShowOverlay = {
    name  = true,  -- 副本名
    level = true,  -- 层数
    score = true,  -- 分数
}
-------------------------------------------------
-- 传送通报开关
-------------------------------------------------
cfg.TeleportAnnounce = {
    enabled = true, -- 传送通报总开关：true=开启, false=关闭
}
----------------------------------------
-- 宝库装等（对应等级 1~10）
----------------------------------------
cfg.ilvls = { 256, 259, 259, 263, 263, 266, 269, 269, 269, 272 }

----------------------------------------
-- 副本名称颜色（周历顶部前 8 条）
----------------------------------------
cfg.RioNameColor = "|cffffffff"

----------------------------------------
-- 副本名称缩写
----------------------------------------
cfg.RioNamesDefault = {
--12.0 Midnight S1
    [239]="执政",[556]="矿坑",[161]="通天",[402]="学院",
	[557]="风行",[558]="平台",[560]="洞窟",[559]="节点",

--旧副本
    [506]="酒庄",[505]="破晨",[504]="暗焰",[503]="回响",[502]="千丝",
    [501]="矶石",[500]="驭雷",[499]="圣焰",[525]="水闸",[542]="生态",
    [400]="诺库德",[401]="碧蓝",[399]="红玉",[405]="蕨皮",
    [403]="奥达曼",[406]="注能",[463]="永上",[464]="永下",
    [375]="仙林",[376]="通灵",[377]="彼界",[378]="赎罪",[379]="凋魂",
    [380]="赤红",[381]="晋升",[382]="伤逝",[391]="天街",[392]="宏图",
    [370]="车间",[369]="垃圾场",[248]="庄园",[244]="阿塔",[251]="地渊",
    [245]="自由",[247]="暴富",[252]="风暴",[250]="神庙",[353]="围攻",
    [234]="卡上",[227]="卡下",[200]="英灵殿",[210]="群星",[198]="黑心",
    [199]="黑鸦",[404]="奈萨鲁斯",[206]="奈萨巢穴",[169]="码头",[166]="车站",
    [165]="影月",[507]="格瑞",[2]="青龙寺",[438]="旋云",[456]="潮汐",[168]="永茂",
	[198]="黑心",[239]="执政",[209]="回廊"
}

----------------------------------------
-- 自动生成副本 mapID 集合
----------------------------------------
cfg.mapIDs = {}
for mapID in pairs(cfg.RioNamesDefault) do
    cfg.mapIDs[mapID] = true
end

----------------------------------------
-- 层数颜色
----------------------------------------
cfg.levelColors = {
    [0] = "ffffffff",
    [1] = "ff1eff00",
    [2] = "ff0070dd",
    [3] = "ffa335ee",
    [4] = "ffff8000",
    [5] = "ffe6cc80",
    [6] = "fff46fc8",
}

----------------------------------------
-- 分数颜色区间
----------------------------------------
cfg.ScoreColorRanges = {
    { minScore = 455, index = 6 },
    { minScore = 410, index = 5 },
    { minScore = 380, index = 4 },
    { minScore = 320, index = 3 },
    { minScore = 230, index = 2 },
    { minScore = 155, index = 1 },
    { minScore = 0,   index = 0 },
}

----------------------------------------
-- 副本传送技能 ID（mapID → spellID）
----------------------------------------
cfg.DungeonTeleport = {
    --Midnight
    [239] = {1254551}, [556] = {1254555}, [161] = {1254557},[557] = {1254400}, 
	[558] = {1254572},[560] = {1254559}, [559] = {1254563},
    -- Cataclysm
    [438] = {410080}, [456] = {424142}, [507] = {445424},
    -- Pandaria
    [2]={131204},[56]={131205},[57]={131225},[58]={131206},[59]={131228},
    [60]={131222},[76]={131232},[77]={131231},[78]={131229},
    -- Draenor
    [161]={159898},[163]={159895},[164]={159897},[165]={159899},
    [166]={159900},[167]={159902},[168]={159901},[169]={159896},
    -- Legion
    [198]={424163},[199]={424153},[200]={393764},[206]={410078},
    [210]={393766},[227]={373262},[234]={373262},
    -- BfA
    [244]={424187},[245]={410071},[247]={467553,467555},
    [248]={424167},[251]={410074},[353]={445418,464256},[369]={373274},[370]={373274},
    -- Shadowlands
    [375]={354464},[376]={354462},[377]={354468},[378]={354465},
    [379]={354463},[380]={354469},[381]={354466},[382]={354467},
    [391]={367416},[392]={367416},
    -- Dragonflight
    [399]={393256},[400]={393262},[401]={393279},[402]={393273},
    [403]={393222},[404]={393276},[405]={393267},[406]={393283},
    [463]={424197},[464]={424197},
    -- The War Within
    [499]={445444},[500]={445443},[501]={445269},[502]={445416},
    [503]={445417},[504]={445441},[505]={445414},[506]={445440},
    [525]={1216786},[542]={1237215},
}

----------------------------------------
--加载项
----------------------------------------
local zMPlus = {}

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LOGIN")
f:RegisterEvent("ADDON_LOADED")

local function InitChallengesUI()
    if not _G["ChallengesFrame"] then
        C_AddOns.LoadAddOn("Blizzard_ChallengesUI")
    end

    if not _G["ChallengesFrame"] then return end

    ChallengesFrame:HookScript("OnShow", function()
        if zMPlus.SetupAndHookDungeonIcons then
            zMPlus:SetupAndHookDungeonIcons()
            zMPlus:UpdateAllDungeonIcons()
        end
    end)

    if zMPlus.SetupAndHookDungeonIcons then
        zMPlus:SetupAndHookDungeonIcons()
        zMPlus:UpdateAllDungeonIcons()
    end
end

f:SetScript("OnEvent", function(_, event, arg)
    if event == "PLAYER_LOGIN" then
        InitChallengesUI()
    elseif event == "ADDON_LOADED" and arg == "Blizzard_ChallengesUI" then
        InitChallengesUI()
    end
end)


------------------------------
--score.lua
------------------------------

local function GetDB()
    return cfg.OverlayText or {}
end

local function GetDungeonName(mapID)
    if cfg.RioNamesDefault and cfg.RioNamesDefault[mapID] then
        return cfg.RioNamesDefault[mapID]
    else
        local info = C_ChallengeMode.GetMapUIInfo(mapID)
        if info and info.name then
            return info.name
        end
    end
    return "未知"
end

local function GetLevelColor(best, overtime)
    if overtime then return "ffa3a3a3" end
    local add = best >= 15 and 1 or 0
    local levelIndex = math.min(math.floor((math.max(best,2)-2)/2),5) + add
    return cfg.levelColors[levelIndex] or cfg.levelColors[0]
end

local function GetScoreColor(score)
    if not score or score == 0 then return cfg.levelColors[0] end
    for _, range in ipairs(cfg.ScoreColorRanges or {}) do
        if score >= range.minScore then
            return cfg.levelColors[range.index] or cfg.levelColors[0]
        end
    end
    return cfg.levelColors[0]
end

local function FormatDungeonInfo(mapID)
    local inTime, overtime = C_MythicPlus.GetSeasonBestForMap(mapID)
    local best, score, ot
    if inTime or overtime then
        local choose = (inTime and overtime and inTime.dungeonScore > overtime.dungeonScore)
                    or (inTime and not overtime)
        best = choose and inTime.level or overtime.level
        score = choose and inTime.dungeonScore or overtime.dungeonScore
        ot = not choose
    else
        best = 0; score = 0; ot = false
    end
    local totalScore = select(2, C_MythicPlus.GetSeasonBestAffixScoreInfoForMap(mapID)) or 0
    local name = GetDungeonName(mapID)
    return name, best, score, totalScore, ot
end

local function CreateTextRegion(parent, line)
    local db = GetDB()[line] or {}
    parent.DIOText = parent.DIOText or {}
    if parent.DIOText[line] then return parent.DIOText[line] end

    local t = parent:CreateFontString(nil,"OVERLAY","GameFontHighlight")
    t:SetPoint("CENTER", parent, "CENTER", db.x or 0, db.y or 0)
    t:SetJustifyH("CENTER")
    t:SetJustifyV("MIDDLE")
    t:SetFont(cfg.GlobalFont or "Fonts\\FRIZQT__.TTF", db.size or 14, cfg.GlobalFontOutline or "OUTLINE")
    parent.DIOText[line] = t
    return t
end

local function HideBuiltInLevel(icon)
    if icon.HighestLevel and icon.HighestLevel.Hide then icon.HighestLevel:Hide() end
end

local function HookIconSetup(icon)
    if icon._DIOHooked then return end
    icon._DIOHooked = true

    if type(icon.SetUp)=="function" then
        hooksecurefunc(icon,"SetUp",function(self)
            HideBuiltInLevel(self)
            if self.mapID and cfg.mapIDs and cfg.mapIDs[self.mapID] then
                local name, best, score, totalScore, overtimeFlag = FormatDungeonInfo(self.mapID)
                CreateTextRegion(self,"name"):SetText("|cffffd700"..name.."|r")
                CreateTextRegion(self,"level"):SetText("|c"..GetLevelColor(best, overtimeFlag)..best.."|r")
                CreateTextRegion(self,"score"):SetText("|c"..GetScoreColor(totalScore)..totalScore.."|r")
            end
        end)
    end

    icon:HookScript("OnShow", function(self) HideBuiltInLevel(self) end)

    HideBuiltInLevel(icon)
    if icon.mapID and cfg.mapIDs and cfg.mapIDs[icon.mapID] then
        local name, best, score, totalScore, overtimeFlag = FormatDungeonInfo(icon.mapID)
        CreateTextRegion(icon,"name"):SetText("|cffffd700"..name.."|r")
        CreateTextRegion(icon,"level"):SetText("|c"..GetLevelColor(best, overtimeFlag)..best.."|r")
        CreateTextRegion(icon,"score"):SetText("|c"..GetScoreColor(totalScore)..totalScore.."|r")
    end
end

function zMPlus:SetupAndHookDungeonIcons()
    if not ChallengesFrame or not ChallengesFrame.DungeonIcons then return end
    for _,icon in ipairs(ChallengesFrame.DungeonIcons) do HookIconSetup(icon) end
    if not self._ChallengesFrameHooked then
        self._ChallengesFrameHooked = true
        if ChallengesFrame.Update and type(ChallengesFrame.Update)=="function" then
            hooksecurefunc(ChallengesFrame,"Update",function()
                for _,icon in ipairs(ChallengesFrame.DungeonIcons) do HookIconSetup(icon) end
            end)
        end
    end
end

function zMPlus:UpdateAllDungeonIcons()
    if not ChallengesFrame or not ChallengesFrame.DungeonIcons then return end
    for _,icon in ipairs(ChallengesFrame.DungeonIcons) do HookIconSetup(icon) end
end


-- weekly.lua

-------------------------------------------------
-- Env & Cache
-------------------------------------------------
local weeklyEnv = {}
weeklyEnv.cache = { weekly = nil, season = nil }

-------------------------------------------------
-- Helpers
-------------------------------------------------
weeklyEnv.colorClass = function(text)
    local _, class = UnitClass("player")
    local c = RAID_CLASS_COLORS[class] or { r=1, g=1, b=1 }
    return string.format("|cff%02x%02x%02x%s|r",
        c.r*255, c.g*255, c.b*255, text)
end

weeklyEnv.colorCompleted = function(completed, level)
    return (completed and "|cff00ff00" or "|cffff0000") .. level .. "|r"
end

local function GetDungeonName(mapID)
    if cfg.RioNamesDefault and cfg.RioNamesDefault[mapID] then
        return cfg.RioNamesDefault[mapID]
    end
    local info = C_ChallengeMode.GetMapUIInfo(mapID)
    return (info and info.name) or ("地图"..mapID)
end

-------------------------------------------------
-- Frame
-------------------------------------------------
local function CreateFrameIfNeeded()
    if weeklyEnv.frame or not PVEFrame then return end

    local f = CreateFrame("Frame", nil, PVEFrame)
    f:SetSize(330, 220)

    local a = cfg.Weekly.anchor
    if not a then a = { point = "TOPLEFT", relativeTo = "PVEFrame", relativePoint = "TOPRIGHT", x = 3, y = 0 } end
    
    f:SetPoint(a.point, _G[a.relativeTo], a.relativePoint, a.x, a.y)

    f.text = f:CreateFontString(nil, "OVERLAY")
    f.text:SetFont(cfg.GlobalFont, cfg.Weekly.fontSize, cfg.GlobalFontOutline)
    f.text:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
    f.text:SetJustifyH("LEFT")

    weeklyEnv.frame = f
end

-------------------------------------------------
-- Common: build dungeon stats (Details)
-------------------------------------------------
local function BuildDungeonStats(runHistory)
    if not cfg.Weekly.showWeeklyDetails then
        return ""
    end

    local byDungeon, order = {}, {}

    for _, run in ipairs(runHistory) do
        local id = run.mapChallengeModeID
        if not byDungeon[id] then
            byDungeon[id] = { name = GetDungeonName(id), runs = {} }
            table.insert(order, id)
        end
        table.insert(byDungeon[id].runs, run)
    end

    table.sort(order)

    local text = weeklyEnv.colorClass("完成次数：")
        .. "|cffff8f00" .. #runHistory .. "|r\n"

    for _, id in ipairs(order) do
        local d = byDungeon[id]
        text = text .. d.name
            .. "|CFF9385ff x|R|CFF0ffff2" .. #d.runs .. "|r - "

        for i, run in ipairs(d.runs) do
            text = text .. weeklyEnv.colorCompleted(run.completed, run.level)
            if i < #d.runs then
                text = text .. "|cff9d9d9d·|r"
            end
        end
        text = text .. "\n"
    end

    return text
end

-------------------------------------------------
-- Weekly Text
-------------------------------------------------
local function BuildWeeklyText(runHistory)
    if not runHistory or #runHistory == 0 then
        return "无本周记录"
    end

    table.sort(runHistory, function(a,b)
        if a.level == b.level then
            return a.mapChallengeModeID < b.mapChallengeModeID
        end
        return a.level > b.level
    end)

    local text = weeklyEnv.colorClass("本周最佳\n")

    for i, run in ipairs(runHistory) do
        if i > 8 then break end
        text = text
            .. weeklyEnv.colorCompleted(run.completed, run.level)
            .. " " .. GetDungeonName(run.mapChallengeModeID)

        if i == 1 or i == 4 or i == 8 then
            text = text .. "|cffa335ee["
                .. (cfg.ilvls[math.min(run.level,10)] or "?")
                .. "装等]|r"
        end
        text = text .. "\n"
    end

    local stats = BuildDungeonStats(runHistory)
    if stats and stats ~= "" then
        text = text .. "\n" .. stats
    end

    return text
end

-------------------------------------------------
-- Season Text
-------------------------------------------------
local function BuildSeasonText(runHistory)
    if not runHistory or #runHistory == 0 then
        return "无赛季记录"
    end

    local bestByDungeon = {}
    for _, run in ipairs(runHistory) do
        if run.completed then
            local id = run.mapChallengeModeID
            if not bestByDungeon[id] or run.level > bestByDungeon[id].level then
                bestByDungeon[id] = run
            end
        end
    end

    local bestList = {}
    for _, run in pairs(bestByDungeon) do
        table.insert(bestList, run)
    end

    table.sort(bestList, function(a,b)
        if a.level == b.level then
            return a.mapChallengeModeID < b.mapChallengeModeID
        end
        return a.level > b.level
    end)

    local text = weeklyEnv.colorClass("赛季记录 \n")

    for i, run in ipairs(bestList) do
        if i > 8 then break end
        text = text
            .. "|cff00ff00" .. run.level .. "|r "
            .. GetDungeonName(run.mapChallengeModeID)
            .. "\n"
    end

    local stats = BuildDungeonStats(runHistory)
    if stats and stats ~= "" then
        text = text .. "\n" .. stats
    end

    return text
end

-------------------------------------------------
-- Cache / Update
-------------------------------------------------
local function InvalidateCache()
    weeklyEnv.cache.weekly = nil
    weeklyEnv.cache.season = nil
end

weeklyEnv.Update = function()
    if not cfg.Weekly.enabled then
        if weeklyEnv.frame then
            weeklyEnv.frame:Hide()
        end
        return
    end

    CreateFrameIfNeeded()
    if not weeklyEnv.frame then return end
    
    weeklyEnv.frame:Show()

    local showSeasonData = false
    if cfg.Weekly.useModifierKey then
        showSeasonData = IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown()
    else
        showSeasonData = false
    end

    local key = showSeasonData and "season" or "weekly"
    
    if not weeklyEnv.cache[key] then
        local history = C_MythicPlus.GetRunHistory(showSeasonData, true)
        
        local textContent = ""
        if showSeasonData then
            textContent = BuildSeasonText(history)
        else
            textContent = BuildWeeklyText(history)
        end
        
        if textContent == "" then
            textContent = "无记录"
        end
        
        weeklyEnv.cache[key] = textContent
    end

    weeklyEnv.frame.text:SetText(weeklyEnv.cache[key])
end

-------------------------------------------------
-- Events
-------------------------------------------------
local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("CHALLENGE_MODE_COMPLETED")
f:RegisterEvent("MODIFIER_STATE_CHANGED")

f:SetScript("OnEvent", function(_, event, arg)
    if event == "ADDON_LOADED" and arg == "OrzUI"
       or event == "PLAYER_ENTERING_WORLD"
       or event == "CHALLENGE_MODE_COMPLETED" then
        InvalidateCache()
        C_Timer.After(1, weeklyEnv.Update)
    elseif event == "MODIFIER_STATE_CHANGED" then
        if cfg.Weekly.enabled and cfg.Weekly.useModifierKey then
            weeklyEnv.Update()
        end
    end
end)

------------------------------
-- Teleport.lua
------------------------------
cfg.DungeonTeleport = cfg.DungeonTeleport or {}
cfg.TeleportAnnounce = cfg.TeleportAnnounce or { enabled = true }

-------------------------------------------------
-- Module
-------------------------------------------------
local Teleport = {}
zMPlus.Teleport = Teleport

local SpellToMap = {}
for mapID, spellIDs in pairs(cfg.DungeonTeleport) do
    for _, spellID in ipairs(spellIDs) do
        SpellToMap[spellID] = mapID
    end
end

-------------------------------------------------
-- Events
-------------------------------------------------
local frame = CreateFrame("Frame")

frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("ADDON_LOADED")
frame:RegisterEvent("LOADING_SCREEN_ENABLED")
frame:RegisterEvent("LOADING_SCREEN_DISABLED")
frame:RegisterEvent("UNIT_SPELLCAST_START")

frame:SetScript("OnEvent", function(_, event, arg1, _, spellID)
    if event == "PLAYER_LOGIN" then
        Teleport:Initialize()

    elseif event == "ADDON_LOADED" and arg1 == "Blizzard_ChallengesUI" then
        Teleport:Initialize()

    elseif event == "LOADING_SCREEN_ENABLED" then
        Teleport.isLoadingScreen = true

    elseif event == "LOADING_SCREEN_DISABLED" then
        Teleport.isLoadingScreen = nil

    elseif event == "UNIT_SPELLCAST_START" then
        Teleport:OnSpellCast(arg1, spellID)
    end
end)

-------------------------------------------------
-- Initialize
-------------------------------------------------
function Teleport:Initialize()
    if self.initialized then return end
    if not C_AddOns.IsAddOnLoaded("Blizzard_ChallengesUI") then return end
    if not cfg.DungeonTeleport then return end

    self.initialized = true

    if ChallengesFrame and type(ChallengesFrame.Update) == "function" then
        hooksecurefunc(ChallengesFrame, "Update", function()
            Teleport:CreateDungeonButtons()
        end)
    end

    self:CreateDungeonButtons()
end

-------------------------------------------------
-- Tooltip
-------------------------------------------------
function Teleport:UpdateGameTooltip(parent, spellID, initialize)
    if self.isLoadingScreen then return end
    if not initialize and not GameTooltip:IsOwned(parent) then return end

    local Button_OnEnter = parent:GetScript("OnEnter")
    if not Button_OnEnter then return end

    local name = C_Spell.GetSpellName(spellID)
    Button_OnEnter(parent)

    GameTooltip:AddLine(" ")

    if IsSpellKnown(spellID) then
        GameTooltip:AddLine(name or "传送到副本")

        local cd = C_Spell.GetSpellCooldown(spellID)
        if not cd.startTime or not cd.duration then
            GameTooltip:AddLine("未知冷却", 1, 0, 0)
        elseif cd.duration == 0 then
            GameTooltip:AddLine("已准备好", 0, 1, 0)
        else
            GameTooltip:AddLine(
                SecondsToTime(math.ceil(cd.startTime + cd.duration - GetTime())),
                1, 0, 0
            )
        end
    else
        GameTooltip:AddLine(name or "传送到副本")
        GameTooltip:AddLine("未学会", 1, 0, 0)
    end

    GameTooltip:Show()

    C_Timer.After(1, function()
        Teleport:UpdateGameTooltip(parent, spellID)
    end)
end

-------------------------------------------------
-- Button
-------------------------------------------------
function Teleport:CreateDungeonButton(icon, spellIDs)
    if not spellIDs then return end
    if InCombatLockdown() then return end

    local spellID = self:SelectBestSpellID(spellIDs)
    if not spellID then return end

    local button = icon.__GVHWA_TeleportButton
    if not button then
        button = CreateFrame("Button", nil, icon, "InsecureActionButtonTemplate")
        button:SetAllPoints(icon)
        button:RegisterForClicks("AnyDown", "AnyUp")
        icon.__GVHWA_TeleportButton = button
    end

    button:SetAttribute("type", "spell")
    button:SetAttribute("spell", spellID)

    button:SetScript("OnEnter", function()
        Teleport:UpdateGameTooltip(icon, spellID, true)
    end)

    button:SetScript("OnLeave", function()
        if GameTooltip:IsOwned(icon) then
            GameTooltip:Hide()
        end
    end)
end

-------------------------------------------------
-- Create All Buttons
-------------------------------------------------
function Teleport:CreateDungeonButtons()
    if InCombatLockdown() then return end
    if not ChallengesFrame or not ChallengesFrame.DungeonIcons then return end

    for _, dungeonIcon in next, ChallengesFrame.DungeonIcons do
        self:CreateDungeonButton(
            dungeonIcon,
            cfg.DungeonTeleport[dungeonIcon.mapID]
        )
    end
end

-------------------------------------------------
-- Spell Select
-------------------------------------------------
function Teleport:SelectBestSpellID(spellIDs)
    for _, spellID in ipairs(spellIDs) do
        if IsSpellKnown(spellID) then
            return spellID
        end
    end
    return spellIDs[1]
end

-------------------------------------------------
-- 传送通报
-------------------------------------------------
local function ShouldAnnounce()
    return IsInGroup() or IsInInstance()
end

local function SafeSendChatMessage(msg, chatType)
    if not msg then return end
    msg = msg:gsub("\n", " ")
    msg = msg:gsub("[%z\1-\31\127]", "")
    msg = msg:sub(1, 255)
    SendChatMessage(msg, chatType)
end

local function GetDungeonName(mapID)
    if not mapID then return "未知副本" end
    local name
    if C_ChallengeMode and C_ChallengeMode.GetMapUIInfo then
        name = C_ChallengeMode.GetMapUIInfo(mapID)
    elseif C_ChallengeMode and C_ChallengeMode.GetMapInfo then
        local info = C_ChallengeMode.GetMapInfo(mapID)
        if info then name = info.name end
    end
    return name or ("副本" .. mapID)
end

function Teleport:GenerateMessage(spellID)
    local mapID = SpellToMap[spellID]
    if not mapID then return end

    local link = C_Spell.GetSpellLink(spellID)
        or ("|cff71d5ff[" .. (GetSpellInfo(spellID) or "?") .. "]|r")

    local dungeonName = GetDungeonName(mapID)
    return ("[2ui]施放%s，传送→%s"):format(link, dungeonName)
end

function Teleport:OnSpellCast(unit, spellID)
    if unit ~= "player" then return end
    if not ShouldAnnounce() then return end
	
    if not cfg.TeleportAnnounce.enabled then return end
	
    local msg = self:GenerateMessage(spellID)
    if not msg then return end

    if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then
        SafeSendChatMessage(msg, "INSTANCE_CHAT")
    elseif IsInGroup() then
        SafeSendChatMessage(msg, "PARTY")
    end
end
