--## Author: MyeolTTeogi ## Version: 12.0.7 (v1.49) ## SavedVariables: WeeklyChecklistDB
local useEnglish = false
local WCLL = {}
local function SetLocale(en)
    useEnglish = en
    if en then
        WCLL.itemLevel       = "Item Level"
        WCLL.flame           = "Spark of Radiance (Craft)"
        WCLL.sepMythic       = "Season 1 Dungeons (Keyless)"
        WCLL.sepPrey         = "Prey"
        WCLL.preyDiff        = { ["普通"]="Normal", ["困难"]="Hard", ["梦魇"]="Nightmare" }
        WCLL.sepRaid         = function(diff) return "Raid ("..diff..")" end
        WCLL.updPrefix       = "Updated: "
        WCLL.crestSuffix     = " Dawncrest"
        WCLL.crestShort      = { [3383]="Adventurer", [3341]="Veteran", [3343]="Champion", [3345]="Hero", [3347]="Myth", [3405]="Field Accolade" }
        WCLL.extraCurrency   = { [515]="Darkmoon Prize Ticket", [1166]="Timewarped Badge", [3363]="Community Coupons" }
        WCLL.diffLabel       = { [14]="Normal", [15]="Heroic", [16]="Mythic", [17]="Raid Finder", [233]="Mythic" }
        WCLL.raidName        = {
            ["虚影尖塔"]          = "The Voidspire",
            ["梦境裂隙"]          = "The Dreamrift",
            ["进军奎尔丹纳斯"]    = "March on Quel'Danas",
            ["孢陨幽径"]          = "Sporefall",
        }
        WCLL.dungName        = {
            ["魔导师平台"]   = "Magisters' Terrace",
            ["节点希纳斯"]   = "Nexus-Point Xenas",
            ["迈萨拉洞窟"]   = "Maisara Caverns",
            ["风行者之塔"]   = "Windrunner Spire",
            ["艾杰斯亚学院"] = "Algeth'ar Academy",
            ["执政团之座"]   = "Seat of the Triumvirate",
            ["通天峰"]       = "The Nexus",
            ["萨隆矿坑"]     = "Pit of Saron",
        }
        WCLL.tipClear        = "Cleared: %d / %d"
        WCLL.tipKilled       = "[Killed]"
        WCLL.tipAlive        = "[Alive]"
        WCLL.cmdList         = "WeeklyChecklist commands:"
        WCLL.cmdOpen         = "Open / Close window"
        WCLL.cmdIcon         = "Show desktop icon"
        WCLL.cmdReset        = "Reset icon position"
        WCLL.cmdRefresh      = "Refresh data"
        WCLL.cmdRefreshAlias = "/wc r"
        WCLL.cmdMClick       = "Open Keystone Dungeon Portal window"
        WCLL.cmdKeyTracker   = "Keystone tracker: Shift+drag=move, Shift+scroll=resize"
        WCLL.msgIconShow     = "Icon shown."
        WCLL.msgIconHide     = "Icon hidden \xE2\x80\x94 type |cFFFFFF00/wc icon|r to show again."
        WCLL.msgReset        = "Icon position reset."
        WCLL.msgRefresh      = "Data refreshed."
        WCLL.msgLoad         = "Loaded \xE2\x80\x94 |cFFFFFF00/wc|r to open  |cFFFFFF00/wc|r (no args) for command list"
        WCLL.ttLClick        = "|cFFFFFFFFLeft-click|r    Open / Close"
        WCLL.ttRClick        = "|cFFFFFFFFRight-click|r   Menu"
        WCLL.ttMClick        = "|cFFFFFFFFMiddle-click|r  Keystone Teleport"
        WCLL.ttWheel         = "|cFFFFFFFFScroll|r        Resize (%dpx)"
        WCLL.ttDrag          = "|cFFFFFFFFDrag|r          Move"
        WCLL.mmTtLClick      = "|cFFFFFFFFLeft-click|r    Open / Close"
        WCLL.mmTtRClick      = "|cFFFFFFFFRight-click|r   Show / Hide icon"
        WCLL.mmTtDrag        = "|cFFFFFFFFDrag|r          Reposition"
        WCLL.menuLocked      = "Lock/Unlock (Locked)"
        WCLL.menuUnlocked    = "Lock/Unlock (Unlocked)"
        WCLL.menuHideIcon    = "Hide Icon"
        WCLL.menuMmOn        = "Minimap Btn: |cFF44FF44ON|r"
        WCLL.menuMmOff       = "Minimap Btn: |cFFFF4444OFF|r"
        WCLL.ldbLClick       = "|cFFFFFFFFLeft-click|r    Open / Close"
        WCLL.ldbRClick       = "|cFFFFFFFFRight-click|r   Show / Hide icon"
        WCLL.btnLang         = "中"
        WCLL.vaultKey        = "Restored Vault Key"
        WCLL.keyFragment     = "Vault Key Fragment"
        WCLL.manaVessel      = "Dawnlight Manaflux (Tier)"
        WCLL.voidCore        = "Nebulous Voidcore (Dice)"
        WCLL.ascendShard     = "Ascendant Voidshard (Upgrade)"
        WCLL.sepCurrency     = "Currencies"
        WCLL.dundunShard     = "Shard of Dundun"
        WCLL.treasureHunterGrace = "Treasure Hunter's Grace"
        WCLL.graceAcquired   = "Acquired"
        WCLL.graceNotYet     = "Not yet"
        WCLL.tipWeeklyCap    = "Weekly Cap: %d / %d"
        WCLL.tipTotalCap     = "Total Cap:  %d / %d"
        WCLL.sepRaidMerged   = "Raid"
        WCLL.sepWorldBoss    = "World Boss"
        WCLL.worldBoss       = "World Boss"
        WCLL.menuHideToon      = "Hide this character"
        WCLL.btnHideMgr        = "Hidden Chars"
        WCLL.hidePanelEmpty    = "(none)"
        WCLL.hideRestore       = "Restore"
        WCLL.btnHideCurrency   = "Hidden Curr"
        WCLL.menuHideCurrency  = "Hide this currency"
        WCLL.hideCurrPanelEmpty = "(none)"
        WCLL.sepDelve        = "Delve"
        WCLL.delveTipTotal   = "Total Clears"
        WCLL.delveTipTier    = "Level %d"
        WCLL.sepOutdoor      = "Delve & Outdoor"
        WCLL.sepMythicPlus   = "Mythic+"
        WCLL.keystone        = "Keystone"
        WCLL.keystoneNone    = "None"
        WCLL.mpNoRuns        = "No runs this week"
        WCLL.mpOnTime        = "in time"
        WCLL.mpNotOnTime     = "depleted"
        WCLL.lastSeen        = "Last seen: "
    else
        WCLL.itemLevel       = "装备等级"
        WCLL.flame           = "光耀火花尘 (制造)"
        WCLL.sepMythic       = "S1 地下城 (无钥匙)"
        WCLL.sepPrey         = "狩猎"
        WCLL.preyDiff        = { ["普通"]="普通", ["困难"]="困难", ["梦魇"]="梦魇" }
        WCLL.sepRaid         = function(diff) return "团本 ("..diff..")" end
        WCLL.updPrefix       = "更新: "
        WCLL.crestSuffix     = " 纹章"
        WCLL.crestShort      = { [3383]="冒险者", [3341]="老兵", [3343]="勇士", [3345]="英雄", [3347]="史诗", [3405]="战地勋章"  }
        WCLL.extraCurrency   = { [515]="暗月奖券", [1166]="时空扭曲徽章", [3363]="社区礼券" }
        WCLL.diffLabel       = { [14]="普通", [15]="英雄", [16]="史诗", [17]="随机", [233]="神话" }
        WCLL.raidName        = {
            ["虚影尖塔"]          = "虚影尖塔",
            ["梦境裂隙"]          = "梦境裂隙",
            ["进军奎尔丹纳斯"]    = "进军奎尔丹纳斯",
            ["孢陨幽径"]          = "孢陨幽径",
        }
        WCLL.dungName        = {
            ["魔导师平台"]   = "魔导师平台",
            ["节点希纳斯"]   = "节点希纳斯",
            ["迈萨拉洞窟"]   = "迈萨拉洞窟",
            ["风行者之塔"]   = "风行者之塔",
            ["艾杰斯亚学院"] = "艾杰斯亚学院",
            ["执政团之座"]   = "执政团之座",
            ["通天峰"]       = "通天峰",
            ["萨隆矿坑"]     = "萨隆矿坑",
        }
        WCLL.tipClear        = "通关: %d / %d"
        WCLL.tipKilled       = "[已击杀]"
        WCLL.tipAlive        = "[未击杀]"
        WCLL.cmdList         = "命令列表:"
        WCLL.cmdOpen         = "打开 / 关闭窗口"
        WCLL.cmdIcon         = "显示桌面图标"
        WCLL.cmdReset        = "重置图标位置"
        WCLL.cmdRefresh      = "刷新数据"
        WCLL.cmdRefreshAlias = "/wc r"
        WCLL.cmdMClick       = "中键点击图标弹出钥石传送窗口"
        WCLL.cmdKeyTracker   = "钥石追踪: Shift+拖动=移动, Shift+滚轮=缩放"
        WCLL.msgIconShow     = "图标已显示"
        WCLL.msgIconHide     = "图标已隐藏 — 输入 |cFFFFFF00/wc icon|r 重新显示"
        WCLL.msgReset        = "图标位置已重置"
        WCLL.msgRefresh      = "数据刷新完成"
        WCLL.msgLoad         = "加载完成 — |cFFFFFF00/wc|r 打开窗口  |cFFFFFF00/wc|r (无参数) 查看命令列表"
        WCLL.ttLClick        = "|cFFFFFFFF左键|r     打开 / 关闭"
        WCLL.ttRClick        = "|cFFFFFFFF右键|r     菜单"
        WCLL.ttMClick        = "|cFFFFFFFF中键|r     钥石传送"
        WCLL.ttWheel         = "|cFFFFFFFF滚轮|r     调整大小 (%dpx)"
        WCLL.ttDrag          = "|cFFFFFFFF拖动|r     移动"
        WCLL.mmTtLClick      = "|cFFFFFFFF左键|r     打开 / 关闭"
        WCLL.mmTtRClick      = "|cFFFFFFFF右键|r     显示 / 隐藏图标"
        WCLL.mmTtDrag        = "|cFFFFFFFF拖动|r     调整位置"
        WCLL.menuLocked      = "锁定↔解锁 (已锁定)"
        WCLL.menuUnlocked    = "锁定↔解锁 (可移动)"
        WCLL.menuHideIcon    = "隐藏图标"
        WCLL.menuMmOn        = "小地图按钮: |cFF44FF44ON|r"
        WCLL.menuMmOff       = "小地图按钮: |cFFFF4444OFF|r"
        WCLL.ldbLClick       = "|cFFFFFFFF左键|r     打开 / 关闭"
        WCLL.ldbRClick       = "|cFFFFFFFF右键|r     显示 / 隐藏图标"
        WCLL.btnLang         = "EN"
        WCLL.vaultKey        = "修复的宝匣钥匙"
        WCLL.keyFragment     = "宝匣钥匙碎片"
        WCLL.manaVessel      = "黎明之光法力容器 (套装)"
        WCLL.voidCore        = "晋升虚空核心 (骰子)"
        WCLL.ascendShard     = "晋升虚空碎片 (强化)"
        WCLL.sepCurrency     = "货币"
        WCLL.dundunShard     = "钝顿碎片"
        WCLL.treasureHunterGrace = "寻宝者的恩泽"
        WCLL.graceAcquired   = "已获得"
        WCLL.graceNotYet     = "未获得"
        WCLL.tipWeeklyCap    = "周上限: %d / %d"
        WCLL.tipTotalCap     = "总上限: %d / %d"
        WCLL.sepRaidMerged   = "团本"
        WCLL.sepWorldBoss    = "世界 BOSS"
        WCLL.worldBoss       = "世界 BOSS"
        WCLL.menuHideToon      = "隐藏该角色"
        WCLL.btnHideMgr        = "已隐角色"
        WCLL.hidePanelEmpty    = "(无)"
        WCLL.hideRestore       = "恢复"
        WCLL.btnHideCurrency   = "已隐货币"
        WCLL.menuHideCurrency  = "隐藏该项"
        WCLL.hideCurrPanelEmpty = "(无)"
        WCLL.sepDelve        = "地下堡"
        WCLL.delveTipTotal   = "通关总数"
        WCLL.delveTipTier    = "%d 级"
        WCLL.sepOutdoor      = "地下堡 & 户外"
        WCLL.sepMythicPlus   = "史诗钥石"
        WCLL.keystone        = "钥石"
        WCLL.keystoneNone    = "无"
        WCLL.mpNoRuns        = "本周无记录"
        WCLL.mpOnTime        = "限时内"
        WCLL.mpNotOnTime     = "超时"
        WCLL.lastSeen        = "最后登录: "
    end
end
SetLocale(false)
local MYTHIC_DUNGEONS = {
    { label = "魔导师平台",   koName = "魔导师平台",   enName = "Magisters' Terrace",   bosses = 4 },
    { label = "节点希纳斯",   koName = "节点希纳斯",   enName = "Nexus-Point Xenas",    bosses = 3 },
    { label = "迈萨拉洞窟",   koName = "迈萨拉洞窟",   enName = "Maisara Caverns",      bosses = 3 },
    { label = "风行者之塔",   koName = "风行者之塔",   enName = "Windrunner Spire",     bosses = 4 },
    { label = "艾杰斯亚学院", koName = "艾杰斯亚学院", enName = "Algeth'ar Academy",    bosses = 4 },
    { label = "执政团之座",   koName = "执政团之座",   enName = "Seat of the Triumvirate", bosses = 4 },
    { label = "通天峰",       koName = "通天峰",       enName = "The Nexus",            bosses = 4 },
    { label = "萨隆矿坑",     koName = "萨隆矿坑",     enName = "Pit of Saron",         bosses = 3 },
}
local KEYSTONE_SHORT_KO = {
    ["魔导师平台"]   = "平台",
    ["节点希纳斯"]   = "节点",
    ["迈萨拉洞窟"]   = "洞窟",
    ["风行者之塔"]   = "风行",
    ["艾杰斯亚学院"] = "学院",
    ["执政团之座"]   = "执政团",
    ["通天峰"]       = "通天峰",
    ["萨隆矿坑"]     = "矿坑",
}
local KEYSTONE_SHORT_EN = {
    ["Magisters' Terrace"]       = "Garden",
    ["Nexus-Point Xenas"]        = "Xenas",
    ["Maisara Caverns"]          = "Maisara",
    ["Windrunner Spire"]         = "Spire",
    ["Algeth'ar Academy"]        = "Academy",
    ["Seat of the Triumvirate"]  = "Triumvirate",
    ["The Nexus"]                = "Nexus",
    ["Pit of Saron"]             = "Saron",
}
local RAIDS = {
    { name = "虚影尖塔",       enName = "The Voidspire",       bosses = 6 },
    { name = "梦境裂隙",       enName = "The Dreamrift",       bosses = 1 },
    { name = "进军奎尔丹纳斯", enName = "March on Quel'Danas", bosses = 2 },
    { name = "孢陨幽径",          enName = "Sporefall",           bosses = 1 },
}
local RAID_DIFFS = {
    { id = 17, label = "随机" },
    { id = 14, label = "普通" },
    { id = 15, label = "英雄" },
    { id = 16, label = "史诗" },
    { id = 233, label = "神话" },
}
local RAID_DIFF_PRIORITY = { 233, 16, 15, 14, 17 }
local MYTHIC_PLUS_VAULT_ILVL = {
    [2]=259, [3]=259, [4]=263, [5]=263, [6]=266,
    [7]=269, [8]=269, [9]=269, [10]=272,
}
local MYTHIC_PLUS_MAX_ILVL = 272
local function GetMythicPlusVaultIlvl(level)
    if not level then return nil end
    if MYTHIC_PLUS_VAULT_ILVL[level] then return MYTHIC_PLUS_VAULT_ILVL[level] end
    if level < 2 then return MYTHIC_PLUS_VAULT_ILVL[2] end
    return MYTHIC_PLUS_MAX_ILVL
end
local function GetMythicPlusVaultSlots(runs)
    local sorted = {}
    for _, r in ipairs(runs) do table.insert(sorted, r) end
    table.sort(sorted, function(a, b) return a.level > b.level end)
    return #sorted, sorted[1], sorted[4], sorted[8]
end
local PREY_DIFFS = {
    {
        key = "普通", bosses = 4,
        questIDs = {
            91124,91123,91122,91121,91120,91119,91118,91117,91116,91115,
            91114,91113,91112,91111,91110,91109,91108,91107,91106,91105,
            91104,91103,91102,91101,91100,91099,91098,91097,91096,91095,
        },
    },
    {
        key = "困难", bosses = 4,
        questIDs = {
            91255,91254,91253,91252,91251,91250,91249,91248,91247,91246,
            91245,91244,91243,91242,91240,91238,91236,91234,91232,91230,
            91228,91226,91224,91222,91220,91218,91216,91214,91212,91210,
        },
    },
    {
        key = "梦魇", bosses = 4,
        questIDs = {
            91269,91268,91267,91266,91265,91264,91263,91262,91261,91260,
            91259,91258,91257,91256,91241,91239,91237,91235,91233,91231,
            91229,91227,91225,91223,91221,91219,91217,91215,91213,91211,
        },
    },
}
local currentDelveTier = nil
local delveProgressSnapshot = {}
local function GetDelveProgressSnapshot()
    local snapshot = {}
    if C_WeeklyRewards and C_WeeklyRewards.GetActivities then
        local activities = C_WeeklyRewards.GetActivities(Enum.WeeklyRewardChestThresholdType.World)
        if activities then
            for _, v in ipairs(activities) do
                if v.type == 6 and v.level and v.level > 0 then
                    snapshot[v.level] = v.progress or 0
                end
            end
        end
    end
    return snapshot
end
local function TryGetDelveTierFromDiff()
    if C_Delves then
        if C_Delves.GetActiveDelveInfo then
            local info = C_Delves.GetActiveDelveInfo()
            if info and info.tier then return info.tier end
        end
        if C_Delves.GetRecommendedDelveTier then
            local tier = C_Delves.GetRecommendedDelveTier()
            if tier and tier > 0 then return tier end
        end
    end
    if C_Scenario.IsInScenario() then
        local function scanForTier(f)
            for _, c in ipairs({f:GetChildren()}) do
                for _, r in ipairs({c:GetRegions()}) do
                    local t = r.GetText and r:GetText()
                    if t then
                        local n = t:match("^(%d+)$")
                        if n then
                            local num = tonumber(n)
                            if num and num >= 1 and num <= 11 then
                                return num
                            end
                        end
                    end
                end
                local result = scanForTier(c)
                if result then return result end
            end
        end
        if ScenarioObjectiveTracker then
            local tier = scanForTier(ScenarioObjectiveTracker)
            if tier then return tier end
        end
    end
    return nil
end
local function DetectCompletedDelveTier()
    local tier = TryGetDelveTierFromDiff()
    if tier then return tier end
    if C_WeeklyRewards and C_WeeklyRewards.GetActivities then
        local activities = C_WeeklyRewards.GetActivities(Enum.WeeklyRewardChestThresholdType.World)
        if activities then
            for _, v in ipairs(activities) do
                if v.type == 6 and v.level and v.level > 0 then
                    local prev = delveProgressSnapshot[v.level] or 0
                    if (v.progress or 0) > prev then
                        return v.level
                    end
                end
            end
        end
    end
    return currentDelveTier
end
local function IsInDelve()
    if C_Delves then
        if C_Delves.IsInDelve then return C_Delves.IsInDelve() end
        if C_Delves.GetActiveDelveInfo then return C_Delves.GetActiveDelveInfo() ~= nil end
    end
    if C_Scenario.IsInScenario() then
        local name = C_Scenario.GetInfo()
        if name and name:find("地下堡") then return true end
    end
    return false
end
local function GetHighestRaidDiff(raidData)
    for _, diffID in ipairs(RAID_DIFF_PRIORITY) do
        local dd = raidData and raidData[diffID]
        if dd and dd.killed and dd.killed > 0 then
            return diffID
        end
    end
    return nil
end
local function GetPreyProgress(questIDs, threshold)
    local count = 0
    for _, qid in ipairs(questIDs) do
        if C_QuestLog.IsQuestFlaggedCompleted(qid) then
            count = count + 1
        end
    end
    return math.min(count, threshold), threshold
end
local RADIANT_FLAME_ITEM_ID = 232875
local FLAME_BASE_YEAR  = 2026
local FLAME_BASE_MONTH = 2
local FLAME_BASE_DAY   = 26
local FLAME_BASE_CAP   = 1
local FLAME_MAX_CAP    = 29
local function GetRadiantFlameCap()
    local baseTime = time({ year=FLAME_BASE_YEAR, month=FLAME_BASE_MONTH, day=FLAME_BASE_DAY, hour=0, min=0, sec=0 })
    local now = time()
    if now < baseTime then return FLAME_BASE_CAP end
    local weeksElapsed = math.floor((now - baseTime) / (7 * 86400))
    return math.min(FLAME_BASE_CAP + weeksElapsed, FLAME_MAX_CAP)
end
local DAWNCREST_CURRENCIES = {
    { id = 3383, short = "冒险者" },
    { id = 3341, short = "老兵"   },
    { id = 3343, short = "勇士"   },
    { id = 3345, short = "英雄"   },
    { id = 3347, short = "史诗"   },
    { id = 3405, short = "神话"   },
}
local EXTRA_CURRENCIES = {
    { id = 515,  cr = {0.79, 0.66, 1.00} },
    { id = 1166, cr = {0.49, 0.78, 0.89} },
    { id = 3363, cr = {0.87, 0.95, 0.81} },
}
local VAULT_KEY_CURRENCY_ID    = 3028
local KEY_FRAGMENT_CURRENCY_ID = 3310
local MANA_VESSEL_CURRENCY_ID  = 3378
local VOID_CORE_CURRENCY_ID    = 3418
local DUNDUN_SHARD_CURRENCY_ID = 3376
local ASCEND_SHARD_ITEM_ID     = 268650
local TREASURE_HUNTER_GRACE_QUEST_ID = 86371
local WORLD_BOSS_QUEST_IDS = {
    92560,
    92123,
    92034,
    92636,
}
local WORLD_BOSS_NAMES = {
    [92560] = { ko = "卢阿莎尔",   en = "Lu'ashal" },
    [92123] = { ko = "岩松",       en = "Cragpine" },
    [92034] = { ko = "索姆贝兰",   en = "Thorm'belan" },
    [92636] = { ko = "普雷达萨斯", en = "Predaxas" },
}
local MAX_LEVEL = 90
local ICON_TEX  = "Interface\\AddOns\\AddOns\\OrzUI\\Media\\Hutu\\star"
local ICON_MIN  = 16
local ICON_MAX  = 128

--[[local WC_SND_JUMP = "Interface\\AddOns\\OrzUI\\Media\\Sounds\\WCjump.ogg"
local WC_SND_DEAD = "Interface\\AddOns\\OrzUI\\Media\\Sounds\\WCdead.ogg"
local WC_SND_SWIM = "Interface\\AddOns\\OrzUI\\Media\\Sounds\\WCswim.ogg"

hooksecurefunc("JumpOrAscendStart", function()
    if not (WeeklyChecklistDB and WeeklyChecklistDB.jumpSound) then return end
    if (IsFlying and IsFlying()) or IsFalling() then return end
    if IsSwimming() or IsSubmerged() then
        PlaySoundFile(WC_SND_SWIM, "SFX")
    else
        PlaySoundFile(WC_SND_JUMP, "SFX")
    end
end)

local wcSoundEvt = CreateFrame("Frame")
wcSoundEvt:RegisterEvent("PLAYER_DEAD")
wcSoundEvt:SetScript("OnEvent", function()
    if not (WeeklyChecklistDB and WeeklyChecklistDB.jumpSound) then return end
    PlaySoundFile(WC_SND_DEAD, "SFX")
end)]]

local kt = {}
local kt_frame
local kt_getPortalSpellID

local function kt_classColor(unit)
    local _, classFile = UnitClass(unit)
    if classFile then
        local cl = C_ClassColor.GetClassColor(classFile)
        if cl then return "|c" .. cl:GenerateHexColor() end
    end
    return "|cFFFFFFFF"
end


local function kt_sendLRSRequest()
    local LibDeflate = LibStub and LibStub:GetLibrary("LibDeflate", true)
    if not LibDeflate then return end
    local data = "J"
    local compressed = LibDeflate:CompressDeflate(data, {level = 9})
    local encoded = LibDeflate:EncodeForWoWAddonChannel(compressed)
    local channel = IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "PARTY"
    pcall(function() C_ChatInfo.SendAddonMessage("LRS", encoded, channel) end)
end

local function kt_broadcastWC()
    local lv, cmid = 0, 0
    pcall(function()
        lv   = C_MythicPlus.GetOwnedKeystoneLevel() or 0
        cmid = C_MythicPlus.GetOwnedKeystoneChallengeMapID() or 0
    end)
    if lv <= 0 then return end
    local channel = IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "PARTY"
    pcall(function() C_ChatInfo.SendAddonMessage("WCKey", "K,"..lv..","..cmid, channel) end)
end

local function kt_buildFrame()
    kt_frame = CreateFrame("Frame", "WCKeyTrackerFrame", UIParent)
    kt_frame:SetSize(220, 40)
    kt_frame:SetPoint("CENTER", UIParent, "CENTER", 0, 200)
    kt_frame:SetFrameStrata("BACKGROUND")
    kt_frame:SetMovable(true)
    kt_frame:EnableMouse(false)
    kt_frame:RegisterForDrag("LeftButton")
    kt_frame:SetClampedToScreen(true)
    kt_frame:RegisterEvent("MODIFIER_STATE_CHANGED")
    kt_frame:RegisterEvent("PLAYER_REGEN_DISABLED")
    kt_frame:SetScript("OnEvent", function(self, event)
        if event == "MODIFIER_STATE_CHANGED" then
            if InCombatLockdown() then return end
            local shifted = IsShiftKeyDown()
            if shifted ~= self._mouseEnabled then
                self._mouseEnabled = shifted
                self:EnableMouse(shifted)
            end
        elseif event == "PLAYER_REGEN_DISABLED" then
            self:StopMovingOrSizing()
            local point, _, relPoint, x, y = self:GetPoint()
            if point then
                WeeklyChecklistDB = WeeklyChecklistDB or {}
                WeeklyChecklistDB.ktPos = {
                    point = point, relPoint = relPoint,
                    x = math.floor(x + 0.5), y = math.floor(y + 0.5)
                }
            end
        end
    end)
    kt_frame:EnableMouseWheel(true)
    kt_frame:SetScript("OnMouseWheel", function(self, delta)
        if not IsShiftKeyDown() then return end
        WeeklyChecklistDB = WeeklyChecklistDB or {}
        local cur = WeeklyChecklistDB.ktScale or 1.0
        local next = math.max(0.5, math.min(2.0, cur + delta * 0.1))
        WeeklyChecklistDB.ktScale = next
        self:SetScale(next)
    end)
    kt_frame:SetScript("OnDragStart", function(self) self:StartMoving() end)
    kt_frame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, relPoint, x, y = self:GetPoint()
        WeeklyChecklistDB.ktPos = {
            point = point, relPoint = relPoint,
            x = math.floor(x + 0.5), y = math.floor(y + 0.5)
        }
    end)
    kt_frame:Hide()
    kt_frame.pool = {}
end

local function kt_getEntry(i)
    if not kt_frame.pool[i] then
        local iconBtn = CreateFrame("Button", nil, kt_frame, "SecureActionButtonTemplate")
        iconBtn:SetSize(24, 24)
        iconBtn:SetAttribute("type", "spell")
        iconBtn:RegisterForClicks("AnyUp", "AnyDown")
        iconBtn:RegisterForDrag("LeftButton")
        iconBtn:SetScript("OnDragStart", function()
            if InCombatLockdown() then return end
            if IsShiftKeyDown() then kt_frame:StartMoving() end
        end)
        iconBtn:SetScript("PreClick", function(self)
            if InCombatLockdown() then return end
            if IsShiftKeyDown() then self:SetAttribute("type", "") end
        end)
        iconBtn:SetScript("PostClick", function(self)
            if InCombatLockdown() then return end
            self:SetAttribute("type", "spell")
        end)
        iconBtn:SetScript("OnDragStop", function()
            if InCombatLockdown() then return end
            kt_frame:StopMovingOrSizing()
            local point, _, relPoint, x, y = kt_frame:GetPoint()
            WeeklyChecklistDB = WeeklyChecklistDB or {}
            WeeklyChecklistDB.ktPos = {
                point = point, relPoint = relPoint,
                x = math.floor(x + 0.5), y = math.floor(y + 0.5)
            }
        end)
        local iconTex = iconBtn:CreateTexture(nil, "ARTWORK")
        iconTex:SetAllPoints()
        local cdOverlay = iconBtn:CreateTexture(nil, "OVERLAY")
        cdOverlay:SetAllPoints()
        cdOverlay:SetColorTexture(0.78, 0.2, 0.2, 0.45)
        cdOverlay:Hide()
        local nameFs = kt_frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        nameFs:SetFont(nameFs:GetFont(), 12, "OUTLINE")
        nameFs:SetJustifyH("LEFT")
        local keyFs = kt_frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        keyFs:SetFont(keyFs:GetFont(), 12, "OUTLINE")
        keyFs:SetJustifyH("LEFT")
        kt_frame.pool[i] = { icon = iconBtn, iconTex = iconTex, cdOverlay = cdOverlay, nameFs = nameFs, keyFs = keyFs }
    end
    return kt_frame.pool[i]
end

local function kt_hideEntry(e)
    e.icon:Hide(); e.nameFs:Hide(); e.keyFs:Hide()
    if e.cdOverlay then e.cdOverlay:Hide() end
end

local function kt_refresh()
    if InCombatLockdown() then return end
    if IsInRaid() then kt_frame:Hide() return end
    local _, instanceType = IsInInstance()
    if (instanceType == "party" or instanceType == "raid") and not WeeklyChecklistDB.enterKeyShown then
        kt_frame:Hide() return
    end
    local myName = UnitName("player")
    local list = {}
    local myLv, myCmid = 0, 0
    pcall(function()
        myLv   = C_MythicPlus.GetOwnedKeystoneLevel() or 0
        myCmid = C_MythicPlus.GetOwnedKeystoneChallengeMapID() or 0
    end)
    if myLv > 0 then
        list[#list+1] = { unit="player", name=myName, level=myLv, cmid=myCmid }
    end
    if IsInGroup() then
        for i = 1, 4 do
            local unit = "party"..i
            if UnitExists(unit) then
                local pname = UnitName(unit)
                if pname then
                    local d = kt[pname]
                    if d and d.level and d.level > 0 then
                        list[#list+1] = { unit=unit, name=pname, level=d.level, cmid=d.cmid or 0, source=d.source }
                    end
                end
            end
        end
    end
    for _, e in ipairs(kt_frame.pool) do kt_hideEntry(e) end
    if #list == 0 then kt_frame:Hide() return end
    local ROW_H = 36
    for idx, p in ipairs(list) do
        local e = kt_getEntry(idx)
        local top = -(idx-1)*ROW_H
        local dungeonName = "未知副本"
        if p.cmid and p.cmid > 0 then
            local dname, _, _, texture = C_ChallengeMode.GetMapUIInfo(p.cmid)
            if texture then e.iconTex:SetTexture(texture); e.iconTex:SetTexCoord(0.05,0.95,0.05,0.95)
            else e.iconTex:SetTexture(nil) end
            if dname and dname ~= "" then dungeonName = dname end
            local spellID = kt_getPortalSpellID(p.cmid)
            if spellID then
                e.icon:SetAttribute("spell", spellID)
                e.icon:EnableMouse(true)
                local cdInfo = C_Spell.GetSpellCooldown(spellID)
                local start = cdInfo and cdInfo.startTime or 0
                local duration = cdInfo and cdInfo.duration or 0
                if start > 0 and duration > 1.5 then
                    e.cdOverlay:Show()
                else
                    e.cdOverlay:Hide()
                end
            else
                e.icon:SetAttribute("spell", nil)
                e.icon:EnableMouse(false)
                e.cdOverlay:Hide()
            end
        else
            e.iconTex:SetTexture(nil)
            e.icon:SetAttribute("spell", nil)
            e.icon:EnableMouse(false)
        end
        e.icon:ClearAllPoints()
        e.icon:SetPoint("TOPLEFT", kt_frame, "TOPLEFT", 0, top-6)
        e.icon:Show()
        e.nameFs:ClearAllPoints()
        e.nameFs:SetPoint("LEFT", e.icon, "RIGHT", 3, 6)
        e.nameFs:SetText(kt_classColor(p.unit)..p.name.."|r")
        e.nameFs:SetWidth(196)
        e.nameFs:Show()
        e.keyFs:ClearAllPoints()
        e.keyFs:SetPoint("LEFT", e.icon, "RIGHT", 3, -8)
        e.keyFs:SetText(string.format("|cFFFFD100%d|r |cFFFFFFFF%s|r", p.level, dungeonName))
        e.keyFs:SetWidth(196)
        e.keyFs:Show()
    end
    kt_frame:SetSize(220, #list*ROW_H)
    kt_frame:Show()
end

local function kt_toggle()
    if IsInRaid() then return end
    if not kt_frame then kt_buildFrame() end
    WeeklyChecklistDB = WeeklyChecklistDB or {}
    if WeeklyChecklistDB.ktShown then
        kt_frame:Hide()
        WeeklyChecklistDB.ktShown = false
    else
        WeeklyChecklistDB.ktShown = true
        kt_refresh()
    end
end

local kt_rosterTimer = nil

local kt_ev = CreateFrame("Frame")
kt_ev:RegisterEvent("PLAYER_LOGIN")
kt_ev:RegisterEvent("CHAT_MSG_ADDON")
kt_ev:RegisterEvent("GROUP_ROSTER_UPDATE")
kt_ev:RegisterEvent("PLAYER_ENTERING_WORLD")
kt_ev:RegisterEvent("UNIT_CONNECTION")
kt_ev:SetScript("OnEvent", function(_, event, ...)
    if event == "PLAYER_LOGIN" then
        C_ChatInfo.RegisterAddonMessagePrefix("LibKS")
        C_ChatInfo.RegisterAddonMessagePrefix("LRS")
        C_ChatInfo.RegisterAddonMessagePrefix("WCKey")
        WeeklyChecklistDB = WeeklyChecklistDB or {}
        if WeeklyChecklistDB.enterKeyShown == nil then WeeklyChecklistDB.enterKeyShown = false end
        if WeeklyChecklistDB.jumpSound == nil then WeeklyChecklistDB.jumpSound = false end
        if not kt_frame then kt_buildFrame() end
        if WeeklyChecklistDB.ktScale then
            kt_frame:SetScale(WeeklyChecklistDB.ktScale)
        end
        local pos = WeeklyChecklistDB.ktPos
        if pos then
            kt_frame:ClearAllPoints()
            kt_frame:SetPoint(pos.point or "CENTER", UIParent, pos.relPoint or "CENTER", pos.x or 0, pos.y or 0)
        end
        if WeeklyChecklistDB.ktShown then kt_refresh() end
    elseif event == "CHAT_MSG_ADDON" then
        local prefix, msg, _, sender = ...
        local name = Ambiguate(sender, "none")
        if prefix == "LRS" then
            local LibDeflate = LibStub and LibStub:GetLibrary("LibDeflate", true)
            if LibDeflate then
                local ok, decoded = pcall(function() return LibDeflate:DecodeForWoWAddonChannel(msg) end)
                if ok and decoded then
                    local ok2, decompressed = pcall(function() return LibDeflate:DecompressDeflate(decoded) end)
                    if ok2 and decompressed and decompressed:sub(1,1) == "K" then
                        local parts = {string.split(",", decompressed)}
                        local lv = tonumber(parts[2])
                        local cmid = tonumber(parts[4])
                        if lv and lv >= 1 and lv <= 30 and cmid then
                            local cur = kt[name]
                            if not cur then
                                kt[name] = { level=lv, cmid=cmid, source="DT" }
                            end
                        end
                    end
                end
            end
        elseif prefix == "LibKS" then
            if msg == "R" then
                local lv, cmid, rating = 0, 0, 0
                pcall(function()
                    lv   = C_MythicPlus.GetOwnedKeystoneLevel() or 0
                    cmid = C_MythicPlus.GetOwnedKeystoneChallengeMapID() or 0
                    local rs = C_PlayerInfo.GetPlayerMythicPlusRatingSummary("player")
                    if rs then rating = math.floor(rs.currentSeasonScore or 0) end
                end)
                if lv > 0 then
                    local ch = IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "PARTY"
                    pcall(function() C_ChatInfo.SendAddonMessage("LibKS", lv..","..cmid..","..rating, ch) end)
                end
            else
                local lvl, mid = msg:match("^(%d+),(%d+),")
                local lv = tonumber(lvl)
                if lv and lv > 0 then
                    local cur = kt[name]
                    if not cur or cur.source == "DT" then
                        kt[name] = { level=lv, cmid=tonumber(mid) or 0, source="BW" }
                    end
                end
            end
        elseif prefix == "ORAddon" then
            if msg:sub(1,1) == "K" then
                local nums = {}
                for n in msg:sub(2):gmatch("%d+") do
                    nums[#nums+1] = tonumber(n)
                    if #nums >= 3 then break end
                end
                local lv = nums[1]
                if lv and lv >= 1 and lv <= 30 then
                    if not kt[name] then
                        kt[name] = { level=lv, cmid=nums[3] or nums[2] or 0, source="DT" }
                    end
                end
            end
        elseif prefix == "WCKey" then
            if msg == "R" then
                kt_broadcastWC()
            elseif msg:sub(1,1) == "K" then
                local lvl, mid = msg:match("^K,(%d+),(%d+)")
                local lv = tonumber(lvl)
                if lv and lv >= 1 and lv <= 30 then
                    local cur = kt[name]
                    if not cur then
                        kt[name] = { level=lv, cmid=tonumber(mid) or 0, source="WC" }
                    end
                end
            end
        end
        if kt_frame and WeeklyChecklistDB.ktShown then kt_refresh() end
    elseif event == "PLAYER_ENTERING_WORLD" then
        kt = {}
        C_Timer.After(2, function()
            if IsInGroup() and not IsInRaid() then
                C_ChatInfo.SendAddonMessage("LibKS", "R", "PARTY")
                kt_sendLRSRequest()
                C_ChatInfo.SendAddonMessage("WCKey", "R", IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "PARTY")
                kt_broadcastWC()
            end
        end)
        if kt_frame and WeeklyChecklistDB.ktShown then
            C_Timer.After(0.5, function() kt_refresh() end)
        end
    elseif event == "UNIT_CONNECTION" then
        local unit, isConnected = ...
        if isConnected then
            C_Timer.After(1, function()
                if IsInGroup() and not IsInRaid() then
                    C_ChatInfo.SendAddonMessage("LibKS", "R", "PARTY")
                    kt_sendLRSRequest()
                    C_ChatInfo.SendAddonMessage("WCKey", "R", IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "PARTY")
                    kt_broadcastWC()
                end
            end)
        end
    elseif event == "GROUP_ROSTER_UPDATE" then
        if kt_rosterTimer then kt_rosterTimer:Cancel() end
        kt_rosterTimer = C_Timer.NewTimer(1, function()
            kt_rosterTimer = nil
            if IsInGroup() and not IsInRaid() then
                local ch = IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "PARTY"
                C_ChatInfo.SendAddonMessage("LibKS", "R", ch)
                C_ChatInfo.SendAddonMessage("WCKey", "R", ch)
                kt_broadcastWC()
            end
        end)
        if kt_frame and WeeklyChecklistDB.ktShown then kt_refresh() end
    end
end)


local iconCfg = { size=48, locked=false, x=100, y=100, visible=true, mmVisible=true, fontSize=11, fontIndex=1 }
local function LoadIconCfg()
    local s = WeeklyChecklistDB.iconCfg
    if s then
        iconCfg.size      = s.size      or iconCfg.size
        iconCfg.x         = s.x         or iconCfg.x
        iconCfg.y         = s.y         or iconCfg.y
        iconCfg.locked    = (s.locked   == 1)
        iconCfg.visible   = (s.visible  ~= 0)
        iconCfg.mmVisible = (s.mmVisible ~= 0)
        iconCfg.fontSize  = s.fontSize  or iconCfg.fontSize
        iconCfg.fontIndex = s.fontIndex or iconCfg.fontIndex
    end
    if WeeklyChecklistDB.useEnglish == 1 then
        SetLocale(true)
    end
end
local function SaveIconCfg()
    WeeklyChecklistDB = WeeklyChecklistDB or {}
    WeeklyChecklistDB.iconCfg = {
        size      = iconCfg.size,
        x         = iconCfg.x,
        y         = iconCfg.y,
        locked    = iconCfg.locked    and 1 or 0,
        visible   = iconCfg.visible   and 1 or 0,
        mmVisible = iconCfg.mmVisible and 1 or 0,
        fontSize  = iconCfg.fontSize,
        fontIndex = iconCfg.fontIndex,
    }
    WeeklyChecklistDB.useEnglish = useEnglish and 1 or 0
end
local function GetToonKey()
    return UnitName("player") .. " - " .. GetRealmName()
end
local BOSS_NAMES_EN = {
    ["魔导师平台"]   = { "Arcanotron Custos", "Seranel Sunlash", "Gemellus", "Degentrius" },
    ["节点希纳斯"]   = { "Chief Corewright Kasreth", "Corewarden Nysarra", "Lothraxion" },
    ["迈萨拉洞窟"]   = { "Muro'jin & Nekraxx", "Vordaza", "Vessel of Souls" },
    ["风行者之塔"]   = { "Emberdawn", "Kalis", "Commander Kroluk", "Restless Heart" },
    ["艾杰斯亚学院"] = { "Overgrown Ancient", "Vexamus", "Crawth", "Echo of Doragosa" },
    ["执政团之座"]   = { "Harbaron", "Ny'alom", "WCLL'ura", "Xil'latath" },
    ["通天峰"]       = { "Commander Stoutbeard / Commander Kolurg", "Anomalus", "Ormorok the Tree-Shaper", "Keristrasza" },
    ["萨隆矿坑"]     = { "Forgemaster Garfrost", "Krick and Ick", "Scourgelord Tyrannus" },
    ["孢陨幽径"]        = { "Rotmire" },
}
local function ReadLockouts()
    local result = {}
    for i = 1, GetNumSavedInstances() do
        local instName, _, _, diffID, isLocked, _, _, isRaid, _, _, numEnc, encProgress = GetSavedInstanceInfo(i)
        if isLocked and instName then
            result[instName] = result[instName] or {}
            local bossList, killed, total = {}, 0, numEnc or 0
            for j = 1, total do
                local bossName, _, isKilled = GetSavedInstanceEncounterInfo(i, j)
                table.insert(bossList, { name = bossName or ("Boss "..j), killed = isKilled })
                if isKilled then killed = killed + 1 end
            end
            if total == 0 then killed = encProgress or 0; total = 1 end
            result[instName][diffID] = { killed=killed, total=total, bosses=bossList, isRaid=isRaid }
        end
    end
    return result
end
local function GetLastWeeklyReset()
    local now = time()
    local t = date("*t", now)
    local region = GetCurrentRegion()
    local resetWday = (region == 1) and 3 or 5
    local daysSinceReset = (t.wday - resetWday + 7) % 7
    if daysSinceReset == 0 and t.hour < 8 then
        daysSinceReset = 7
    end
    local todayMidnight = now - (t.hour * 3600 + t.min * 60 + t.sec)
    local lastResetMidnight = todayMidnight - daysSinceReset * 86400
    return lastResetMidnight + 8 * 3600
end
local function GetLastDailyReset()
    local now = time()
    local t = date("*t", now)
    local todayMidnight = now - (t.hour * 3600 + t.min * 60 + t.sec)
    if t.hour < 8 then
        return todayMidnight - 86400 + 8 * 3600
    else
        return todayMidnight + 8 * 3600
    end
end
local function ClearDungeonData(t)
    t.dungeons = {}
    for _, d in ipairs(MYTHIC_DUNGEONS) do
        t.dungeons[d.label] = { killed=0, total=d.bosses, bosses={} }
    end
    t.dungeonReset = GetLastDailyReset()
end
local function ClearWeeklyData(t)
    if t.lastUpdate then
        t.lastSession = {
            time            = t.lastUpdate,
            itemLevel       = t.itemLevel,
            dungeons        = t.dungeons,
            raids           = t.raids,
            prey            = t.prey,
            mythicPlus      = t.mythicPlus,
            delves          = t.delves,
            worldBossKilled = t.worldBossKilled,
            worldBossName   = t.worldBossName,
            radiantFlame    = t.radiantFlame,
            dawncrest       = t.dawncrest,
            vaultKey        = t.vaultKey,
            keyFragment     = t.keyFragment,
            manaVessel      = t.manaVessel,
            voidCore        = t.voidCore,
            ascendShard     = t.ascendShard,
            itemTooltips    = t.itemTooltips,
            dundunShard     = t.dundunShard,
            gold            = t.gold,
            treasureHunterGrace = t.treasureHunterGrace,
            keystone        = t.keystone,
        }
    end
    ClearDungeonData(t)
    t.raids = {}
    for _, r in ipairs(RAIDS) do
        t.raids[r.name] = {}
        for _, diff in ipairs(RAID_DIFFS) do
            t.raids[r.name][diff.id] = { killed=0, total=r.bosses, bosses={} }
        end
    end
    t.prey = {}
    for _, d in ipairs(PREY_DIFFS) do
        t.prey[d.key] = { killed=0, total=d.bosses }
    end
    t.fragBaseQuantity = nil
    t.keyBaseEarned    = nil
    t.fragResetTime    = GetLastWeeklyReset()
    t.weeklyReset      = GetLastWeeklyReset()
    t.worldBossKilled  = 0
    t.delves           = { total = 0, tiers = {} }
    t.delveReset       = GetLastWeeklyReset()
    t.mythicPlus           = { runs = {} }
    t.mythicPlusReset      = GetLastWeeklyReset()
    t.treasureHunterGrace  = 0
    t.keystone             = nil
end
local function ResetExpiredWeeklyData()
    WeeklyChecklistDB = WeeklyChecklistDB or {}
    local lastWeeklyReset = GetLastWeeklyReset()
    local lastDailyReset  = GetLastDailyReset()
    for _, toon in pairs(WeeklyChecklistDB) do
        if type(toon) == "table" and toon.level then
            local weeklyAlreadyReset = toon.weeklyReset and toon.weeklyReset >= lastWeeklyReset
            if not weeklyAlreadyReset then
                if not toon.lastUpdate or toon.lastUpdate < lastWeeklyReset then
                    ClearWeeklyData(toon)
                end
            end
            local dungeonAlreadyReset = toon.dungeonReset and toon.dungeonReset >= lastDailyReset
            if not dungeonAlreadyReset then
                if not toon.lastUpdate or toon.lastUpdate < lastDailyReset then
                    ClearDungeonData(toon)
                end
            end
            local delveAlreadyReset = toon.delveReset and toon.delveReset >= lastWeeklyReset
            if not delveAlreadyReset then
                toon.delves     = { total = 0, tiers = {} }
                toon.delveReset = lastWeeklyReset
            end
            local mpAlreadyReset = toon.mythicPlusReset and toon.mythicPlusReset >= lastWeeklyReset
            if not mpAlreadyReset then
                toon.mythicPlus      = { runs = {} }
                toon.mythicPlusReset = lastWeeklyReset
            end
        end
    end
end
local function CollectData()
    WeeklyChecklistDB = WeeklyChecklistDB or {}
    local key = GetToonKey()
    WeeklyChecklistDB[key] = WeeklyChecklistDB[key] or {}
    local t = WeeklyChecklistDB[key]
    t.name      = UnitName("player")
    t.level     = UnitLevel("player")
    t.class     = select(2, UnitClass("player"))
    t.itemLevel = math.floor(select(2, GetAverageItemLevel()) * 10) / 10
    local lockouts = ReadLockouts()
    t.dungeons = {}
    for _, d in ipairs(MYTHIC_DUNGEONS) do
        local src = lockouts[d.koName] or lockouts[d.enName]
        local info = src and (src[23] or src[8])
        if not info and src then for _, v in pairs(src) do info = v; break end end
        if info then
            t.dungeons[d.label] = { killed=info.killed, total=info.total, bosses=info.bosses }
        else
            t.dungeons[d.label] = { killed=0, total=d.bosses, bosses={} }
        end
    end
    t.raids = {}
    for _, r in ipairs(RAIDS) do
        t.raids[r.name] = {}
        local src = lockouts[r.name] or lockouts[r.enName] or {}
        for _, diff in ipairs(RAID_DIFFS) do
            local info = src[diff.id]
            if info then
                t.raids[r.name][diff.id] = { killed=info.killed, total=info.total, bosses=info.bosses }
            else
                t.raids[r.name][diff.id] = { killed=0, total=r.bosses, bosses={} }
            end
        end
    end
    t.radiantFlame = C_Item.GetItemCount(RADIANT_FLAME_ITEM_ID, true)
    t.dawncrest = {}
    for _, c in ipairs(DAWNCREST_CURRENCIES) do
        local info = C_CurrencyInfo.GetCurrencyInfo(c.id)
        t.dawncrest[c.id] = info and info.quantity or 0
    end
    for _, c in ipairs(EXTRA_CURRENCIES) do
        local info = C_CurrencyInfo.GetCurrencyInfo(c.id)
        t.dawncrest[c.id] = info and info.quantity or 0
    end
    if VAULT_KEY_CURRENCY_ID ~= 0 then
        local keyInfo = C_CurrencyInfo.GetCurrencyInfo(VAULT_KEY_CURRENCY_ID)
        t.vaultKey = keyInfo and keyInfo.quantity or 0
    else
        t.vaultKey = 0
    end
    if KEY_FRAGMENT_CURRENCY_ID ~= 0 then
        local fragInfo       = C_CurrencyInfo.GetCurrencyInfo(KEY_FRAGMENT_CURRENCY_ID)
        local fragCurrent    = fragInfo and fragInfo.quantity or 0
        local keyInfo2       = C_CurrencyInfo.GetCurrencyInfo(VAULT_KEY_CURRENCY_ID)
        local keyTotalEarned = keyInfo2 and keyInfo2.totalEarned or 0
        local lastReset      = GetLastWeeklyReset()
        if not t.fragResetTime or t.fragResetTime < lastReset then
            t.fragBaseQuantity = fragCurrent
            t.keyBaseEarned    = keyTotalEarned
            t.fragResetTime    = lastReset
        end

        local fragDelta    = fragCurrent    - (t.fragBaseQuantity or fragCurrent)
        local keyDelta     = (keyTotalEarned - (t.keyBaseEarned    or keyTotalEarned)) * 100
        local weeklyEarned = math.max(0, fragDelta + keyDelta)
        t.keyFragment = { current = fragCurrent, weekly = weeklyEarned }
    else
        t.keyFragment = { current = 0, weekly = 0 }
    end
    if MANA_VESSEL_CURRENCY_ID ~= 0 then
        local vesselInfo  = C_CurrencyInfo.GetCurrencyInfo(MANA_VESSEL_CURRENCY_ID)
        local vesselCur   = vesselInfo and vesselInfo.quantity     or 0
        local vesselTotal = vesselInfo and vesselInfo.totalEarned  or 0
        t.manaVessel = { current = vesselCur, totalEarned = vesselTotal }
    else
        t.manaVessel = { current = 0, totalEarned = 0 }
    end
    if VOID_CORE_CURRENCY_ID ~= 0 then
        local vcInfo  = C_CurrencyInfo.GetCurrencyInfo(VOID_CORE_CURRENCY_ID)
        local vcCur   = vcInfo and vcInfo.quantity    or 0
        local vcTotal = vcInfo and vcInfo.totalEarned or 0
        t.voidCore = { current = vcCur, totalEarned = vcTotal }
    else
        t.voidCore = { current = 0, totalEarned = 0 }
    end
    t.ascendShard = C_Item.GetItemCount(ASCEND_SHARD_ITEM_ID, true)
    if DUNDUN_SHARD_CURRENCY_ID ~= 0 then
        local di = C_CurrencyInfo.GetCurrencyInfo(DUNDUN_SHARD_CURRENCY_ID)
        t.dundunShard = {
            current      = di and di.quantity               or 0,
            weeklyEarned = di and di.quantityEarnedThisWeek or 0,
            maxWeekly    = di and di.maxWeeklyQuantity       or 8,
            maxTotal     = di and di.maxQuantity             or 8,
        }
    else
        t.dundunShard = { current=0, weeklyEarned=0, maxWeekly=8, maxTotal=8 }
    end
    t.prey = {}
    for _, d in ipairs(PREY_DIFFS) do
        local k, n = GetPreyProgress(d.questIDs, d.bosses)
        t.prey[d.key] = { killed=k, total=n }
    end
    local wbKilled = false
    local wbActiveName = nil
    for _, qid in ipairs(WORLD_BOSS_QUEST_IDS) do
        if C_QuestLog.IsQuestFlaggedCompleted(qid) then
            wbKilled = true
            wbActiveName = WORLD_BOSS_NAMES[qid]
            break
        end
    end
    if not wbActiveName then
        for _, qid in ipairs(WORLD_BOSS_QUEST_IDS) do
            if C_TaskQuest.IsActive(qid) then
                wbActiveName = WORLD_BOSS_NAMES[qid]
                break
            end
        end
    end
    t.worldBossKilled = wbKilled and 1 or 0
    t.worldBossName   = wbActiveName
    t.gold = GetMoney()
    t.treasureHunterGrace = C_QuestLog.IsQuestFlaggedCompleted(TREASURE_HUNTER_GRACE_QUEST_ID) and 1 or 0
    do
        local ksMapID = C_MythicPlus and C_MythicPlus.GetOwnedKeystoneChallengeMapID and C_MythicPlus.GetOwnedKeystoneChallengeMapID()
        local ksLevel = C_MythicPlus and C_MythicPlus.GetOwnedKeystoneLevel and C_MythicPlus.GetOwnedKeystoneLevel()
        if ksMapID and ksLevel then
            local ksName = ""
            if C_ChallengeMode and C_ChallengeMode.GetMapUIInfo then
                ksName = C_ChallengeMode.GetMapUIInfo(ksMapID) or ""
            end
            t.keystone = { level = ksLevel, mapName = ksName }
        else
            t.keystone = nil
        end
    end
    if not t.delves then
        t.delves = { total = 0, tiers = {} }
    end
    do
        local apiRuns = C_MythicPlus and C_MythicPlus.GetRunHistory and C_MythicPlus.GetRunHistory(false, true)
        if apiRuns and #apiRuns > 0 then
            t.mythicPlus = { runs = {} }
            for _, run in ipairs(apiRuns) do
                local mapName = ""
                if C_ChallengeMode and C_ChallengeMode.GetMapUIInfo then
                    local n = C_ChallengeMode.GetMapUIInfo(run.mapChallengeModeID)
                    mapName = n or ""
                end
                table.insert(t.mythicPlus.runs, {
                    level   = run.level,
                    mapID   = run.mapChallengeModeID,
                    mapName = mapName,
                    onTime  = run.completed,
                    time    = run.durationSec,
                })
            end
        elseif not t.mythicPlus then
            t.mythicPlus = { runs = {} }
        end
    end
    do
        local summary = C_PlayerInfo and C_PlayerInfo.GetPlayerMythicPlusRatingSummary and
                        C_PlayerInfo.GetPlayerMythicPlusRatingSummary("player")
        t.mpRating = (summary and summary.currentSeasonScore) or 0
        t.mpBestPerMap = {}
        if C_ChallengeMode and C_ChallengeMode.GetMapTable and
           C_MythicPlus and C_MythicPlus.GetSeasonBestAffixScoreInfoForMap then
            local mapIDs = C_ChallengeMode.GetMapTable()
            if mapIDs then
                for _, mapID in ipairs(mapIDs) do
                    local mapName = C_ChallengeMode.GetMapUIInfo(mapID) or ""
                    if mapName ~= "" then
                        local affixScores = C_MythicPlus.GetSeasonBestAffixScoreInfoForMap(mapID)
                        local best = 0
                        if affixScores then
                            for _, info in ipairs(affixScores) do
                                if info.level and info.level > best then
                                    best = info.level
                                end
                            end
                        end
                        if best > 0 then
                            t.mpBestPerMap[mapName] = best
                        end
                    end
                end
            end
        end
    end
    t.lastSeen   = time()
    t.lastUpdate = time()
    local wc_scrape = CreateFrame("GameTooltip", "WCScrapeTip", UIParent, "GameTooltipTemplate")
    wc_scrape:SetOwner(UIParent, "ANCHOR_NONE")
    local function scrapeLines(id)
        wc_scrape:SetCurrencyByID(id)
        local out = {}
        for i = 1, wc_scrape:NumLines() do
            local L2 = _G["WCScrapeTipTextLeft"..i]
            local R2 = _G["WCScrapeTipTextRight"..i]
            local lt = L2 and L2:GetText() or ""
            local rt = R2 and R2:GetText() or ""
            if lt ~= "" or rt ~= "" then
                local lr, lg, lb = 1, 1, 1
                local rr, rg, rb = 1, 1, 1
                if L2 then lr, lg, lb = L2:GetTextColor() end
                if R2 then rr, rg, rb = R2:GetTextColor() end
                table.insert(out, { l=lt, r=(rt~="" and rt or nil), lr=lr, lg=lg, lb=lb, rr=rr, rg=rg, rb=rb })
            end
        end
        return out
    end
    t.currencyTooltips = {}
    local ids = { 3212, VAULT_KEY_CURRENCY_ID, KEY_FRAGMENT_CURRENCY_ID, MANA_VESSEL_CURRENCY_ID, VOID_CORE_CURRENCY_ID, DUNDUN_SHARD_CURRENCY_ID }
    for _, c in ipairs(DAWNCREST_CURRENCIES) do table.insert(ids, c.id) end
    for _, c in ipairs(EXTRA_CURRENCIES) do table.insert(ids, c.id) end
    for _, id in ipairs(ids) do
        if id and id ~= 0 then
            t.currencyTooltips[id] = scrapeLines(id)
        end
    end
    local function scrapeItemLines(itemID)
        wc_scrape:SetHyperlink("item:"..itemID)
        local out = {}
        for i = 1, wc_scrape:NumLines() do
            local L2 = _G["WCScrapeTipTextLeft"..i]
            local R2 = _G["WCScrapeTipTextRight"..i]
            local lt = L2 and L2:GetText() or ""
            local rt = R2 and R2:GetText() or ""
            if lt ~= "" or rt ~= "" then
                local lr, lg, lb = 1, 1, 1
                local rr, rg, rb = 1, 1, 1
                if L2 then lr, lg, lb = L2:GetTextColor() end
                if R2 then rr, rg, rb = R2:GetTextColor() end
                table.insert(out, { l=lt, r=(rt~="" and rt or nil), lr=lr, lg=lg, lb=lb, rr=rr, rg=rg, rb=rb })
            end
        end
        return out
    end
    t.itemTooltips = {}
    t.itemTooltips[ASCEND_SHARD_ITEM_ID] = scrapeItemLines(ASCEND_SHARD_ITEM_ID)
    wc_scrape:Hide()
end
local C = {
    bg       = {0.04,0.04,0.05,0.97},
    header   = {0.07,0.07,0.09,1.00},
    sepBg    = {0.10,0.08,0.04,1.00},
    rowOdd   = {0.00,0.00,0.00,0.00},
    rowEven  = {1.00,1.00,1.00,0.025},
    line     = {0.22,0.20,0.16,0.60},
    border   = {0.20,0.20,0.22,1.00},
    titleBg  = {0.06,0.06,0.08,1.00},
    txtNormal= {0.82,0.82,0.82},
    txtHeader= {0.95,0.85,0.45},
    txtDung  = {0.88,0.88,0.88},
    txtRaid  = {1.00,0.60,0.60},
    txtRaidLabel = {1.00,0.78,0.78},
    txtInfo  = {0.45,0.82,1.00},
    txtFlame = {1.00,0.58,0.18},
    txtCrest = {0.50,0.88,0.50},
    kill     = {0.15,1.00,0.30},
    partial  = {1.00,0.82,0.08},
    zero     = {0.42,0.42,0.42},
}
local ROW_H=22; local NAME_W=184; local TOON_W=88; local PAD_L=10; local PAD_T=46
local FONT_S=11; local FONT_XS=10
local FONT_SIZE_MIN=8; local FONT_SIZE_MAX=18
local FONT_LIST = {}
local function BuildFontList()
    FONT_LIST = {}
    local defaultFile = select(1, GameFontNormal:GetFont())
    local LSM = LibStub and LibStub("LibSharedMedia-3.0", true)
    if LSM then
        for _, name in ipairs(LSM:List("font")) do
            local file = LSM:Fetch("font", name)
            if file then
                table.insert(FONT_LIST, { name = name, file = file })
            end
        end
    end
    if #FONT_LIST == 0 then
        local builtins = {
            { name = "默认",   obj = GameFontNormal  },
            { name = "聊天",   obj = ChatFontNormal   },
            { name = "数字",   obj = NumberFontNormal },
            { name = "任务",   obj = QuestFont        },
        }
        for _, entry in ipairs(builtins) do
            local ok, file = pcall(function() return select(1, entry.obj:GetFont()) end)
            if ok and file then
                table.insert(FONT_LIST, { name = entry.name, file = file })
            end
        end
    end
    if #FONT_LIST == 0 then
        table.insert(FONT_LIST, { name = "默认", file = defaultFile })
    end
    local defaultIdx = 1
    for i, entry in ipairs(FONT_LIST) do
        if entry.file == defaultFile then defaultIdx = i; break end
    end
    if defaultIdx ~= 1 then
        local matched = table.remove(FONT_LIST, defaultIdx)
        if not matched.name:find("%(默认%)") then
            matched.name = matched.name .. " (默认)"
        end
        table.insert(FONT_LIST, 1, matched)
    else
        if not FONT_LIST[1].name:find("%(默认%)") then
            FONT_LIST[1].name = FONT_LIST[1].name .. " (默认)"
        end
    end
end
local function FF(size)
    local entry = FONT_LIST[iconCfg.fontIndex] or FONT_LIST[1]
    local file = (entry and entry.file) or select(1, GameFontNormal:GetFont())
    return file, (size or FONT_S)
end
local function Lbl(parent, text, r,g,b, justify, size)
    local fs = parent:CreateFontString(nil,"OVERLAY")
    fs:SetFont(FF(size or FONT_S)); fs:SetText(text or "")
    fs:SetTextColor(r,g,b); fs:SetJustifyH(justify or "LEFT")
    return fs
end
local function GetCC(tok)
    local c = tok and RAID_CLASS_COLORS[tok]
    return c and c.r or 0.8, c and c.g or 0.8, c and c.b or 0.8
end
local function ProgColor(k, total)
    if k==0 then return unpack(C.zero) elseif k>=total then return unpack(C.kill) else return unpack(C.partial) end
end
local wcTip
local wcToonMenu
local wcToonMenuKey
local wcCurrRowMenu
local wcCurrRowMenuKey
local wcCurrRowMenuLabel
local function ShowTip(anchor, label, bossList, k, total, dungKoName)
    if not wcTip then
        wcTip = CreateFrame("GameTooltip","WCTip",UIParent,"GameTooltipTemplate")
        wcTip:SetFrameStrata("TOOLTIP")
    end
    wcTip:SetOwner(anchor,"ANCHOR_RIGHT"); wcTip:ClearLines()
    wcTip:AddLine(label, 1,0.9,0.4)
    wcTip:AddLine(string.format(WCLL.tipClear, k, total), 0.75,0.75,0.75)
    if bossList and #bossList > 0 then
        wcTip:AddLine(" ")
        local enNames = useEnglish and dungKoName and BOSS_NAMES_EN[dungKoName]
        for idx, b in ipairs(bossList) do
            local displayName = (enNames and enNames[idx]) or b.name or "?"
            if b.killed then wcTip:AddLine("|cFF33FF55"..WCLL.tipKilled.."|r  "..displayName)
            else              wcTip:AddLine("|cFF888888"..WCLL.tipAlive.."|r  "..displayName) end
        end
    end
    wcTip:Show()
end
local function ShowCurrencyTip(anchor, currencyID)
    if not wcTip then
        wcTip = CreateFrame("GameTooltip","WCTip",UIParent,"GameTooltipTemplate")
        wcTip:SetFrameStrata("TOOLTIP")
    end
    wcTip:SetOwner(anchor, "ANCHOR_RIGHT")
    wcTip:SetCurrencyByID(currencyID)
    wcTip:Show()
end
local function ShowSavedCurrencyTip(anchor, lines)
    if not lines or #lines == 0 then return end
    if not wcTip then
        wcTip = CreateFrame("GameTooltip","WCTip",UIParent,"GameTooltipTemplate")
        wcTip:SetFrameStrata("TOOLTIP")
    end
    wcTip:SetOwner(anchor, "ANCHOR_RIGHT")
    wcTip:SetWidth(260)
    wcTip:ClearLines()
    for _, line in ipairs(lines) do
        if line.r then
            wcTip:AddDoubleLine(line.l, line.r, line.lr, line.lg, line.lb, line.rr, line.rg, line.rb)
        else
            wcTip:AddLine(line.l, line.lr, line.lg, line.lb, true)
        end
    end
    wcTip:Show()
end
local RAID_TIP_ORDER = { 17, 14, 15, 16, 233 }
local function ShowRaidMergedTip(anchor, raidLabel, raidData)
    if not wcTip then
        wcTip = CreateFrame("GameTooltip","WCTip",UIParent,"GameTooltipTemplate")
        wcTip:SetFrameStrata("TOOLTIP")
    end
    wcTip:SetOwner(anchor, "ANCHOR_RIGHT"); wcTip:ClearLines()
    wcTip:AddLine(raidLabel, 1, 0.9, 0.4)
    local hasAny = false
    for _, diffID in ipairs(RAID_TIP_ORDER) do
        local dd = raidData and raidData[diffID]
        if dd and dd.total and dd.total > 0 and (dd.killed or 0) > 0 then
            local diffLabel = WCLL.diffLabel[diffID] or tostring(diffID)
            local k = dd.killed or 0
            local n = dd.total
            local kr, kg, kb = ProgColor(k, n)
            if hasAny then wcTip:AddLine(" ") end
            wcTip:AddLine(diffLabel..":  "..k.." / "..n, kr, kg, kb)
            if dd.bosses and #dd.bosses > 0 then
                for _, b in ipairs(dd.bosses) do
                    if b.killed then
                        wcTip:AddLine("|cFF33FF55"..WCLL.tipKilled.."|r  "..(b.name or "?"))
                    else
                        wcTip:AddLine("|cFF888888"..WCLL.tipAlive.."|r  "..(b.name or "?"))
                    end
                end
            end
            hasAny = true
        end
    end
    if not hasAny then
        wcTip:AddLine(WCLL.mpNoRuns, 0.5, 0.5, 0.5)
    end
    wcTip:Show()
end
local function ShowDelveTip(anchor, toonName, delveData)
    if not wcTip then
        wcTip = CreateFrame("GameTooltip","WCTip",UIParent,"GameTooltipTemplate")
        wcTip:SetFrameStrata("TOOLTIP")
    end
    wcTip:SetOwner(anchor, "ANCHOR_RIGHT"); wcTip:ClearLines()
    wcTip:AddLine((useEnglish and "Delve" or "地下堡").." — "..(toonName or "?"), 1, 0.9, 0.4)
    wcTip:AddLine(" ")
    local total = (delveData and delveData.total) or 0
    wcTip:AddDoubleLine(WCLL.delveTipTotal, total..(useEnglish and "x" or "次"), 0.75,0.75,0.75, 0.2,1.0,0.35)
    local tiers = (delveData and delveData.tiers) or {}
    local sortedTiers = {}
    for tier, count in pairs(tiers) do
        if count > 0 then
            table.insert(sortedTiers, { tier=tier, count=count })
        end
    end
    table.sort(sortedTiers, function(a,b) return a.tier > b.tier end)
    if #sortedTiers > 0 then
        wcTip:AddLine(" ")
        for _, entry in ipairs(sortedTiers) do
            local tierLabel = string.format(WCLL.delveTipTier, entry.tier)
            wcTip:AddDoubleLine(tierLabel, entry.count..(useEnglish and "x" or "次"), 0.88,0.88,0.88, 0.2,1.0,0.35)
        end
    end
    wcTip:Show()
end
local function ShowMythicPlusTip(anchor, toonName, mpData)
    if not wcTip then
        wcTip = CreateFrame("GameTooltip","WCTip",UIParent,"GameTooltipTemplate")
        wcTip:SetFrameStrata("TOOLTIP")
    end
    wcTip:SetOwner(anchor, "ANCHOR_RIGHT"); wcTip:ClearLines()
    wcTip:AddLine(toonName or "?", 1, 0.9, 0.4)
    local runs = (mpData and mpData.runs) or {}
    local sorted = {}
    for _, r in ipairs(runs) do table.insert(sorted, r) end
    table.sort(sorted, function(a, b) return (a.level or 0) > (b.level or 0) end)
    if #sorted == 0 then
        wcTip:AddLine(WCLL.mpNoRuns, 0.5, 0.5, 0.5)
    else
        wcTip:AddLine(useEnglish and "Top 8 dungeons" or "前 8 个副本", 0.53, 0.53, 0.53)
        for i, r in ipairs(sorted) do
            if i > 8 then break end
            local cr, cg, cb = r.onTime and 0.15 or 1, r.onTime and 1 or 0.27, r.onTime and 0.19 or 0.27
            local ilvlSuffix = ""
            if i == 1 or i == 4 or i == 8 then
                local ilvl = GetMythicPlusVaultIlvl(r.level)
                if ilvl then ilvlSuffix = " |cFFFFD700iLvl " .. ilvl .. "|r" end
            end
            wcTip:AddLine("+"..(r.level or "?").."  "..(r.mapName or "?")..ilvlSuffix, cr, cg, cb)
        end
        if #sorted > 8 then
            wcTip:AddLine("(+"..tostring(#sorted-8)..(useEnglish and " more)" or " 次更多)"), 0.5, 0.5, 0.5)
        end
    end
    wcTip:Show()
end
local function ProgBtn(parent, xOff, k, total, bosses, label, dungKoName)
    local r,g,b = ProgColor(k, total)
    local btn = CreateFrame("Frame", nil, parent)
    btn:SetPoint("TOPLEFT", parent, "TOPLEFT", xOff, 0); btn:SetSize(TOON_W, ROW_H); btn:EnableMouse(true)
    local fs = btn:CreateFontString(nil,"OVERLAY"); fs:SetFont(FF(FONT_S))
    fs:SetText(k.."/"..total); fs:SetTextColor(r,g,b); fs:SetJustifyH("CENTER"); fs:SetAllPoints()
    btn:SetScript("OnEnter", function(self) ShowTip(self, label, bosses, k, total, dungKoName) end)
    btn:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
    return btn
end
local mainFrame
local wcIcon
local mmBtn
local function FormatGold(copper)
    if not copper or copper == 0 then return "0|TInterface\\MoneyFrame\\UI-GoldIcon:0|t" end
    local g = math.floor(copper / 10000)
    local s = math.floor((copper % 10000) / 100)
    local c = copper % 100
    local function AddCommas(n)
        local str = tostring(n)
        return str:reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
    end
    local out = ""
    if g > 0 then
        out = out .. "|cFFFFD700" .. AddCommas(g) .. "|r|TInterface\\MoneyFrame\\UI-GoldIcon:0|t "
    end
    if s > 0 or g > 0 then
        out = out .. "|cFFC0C0C0" .. string.format("%d", s) .. "|r|TInterface\\MoneyFrame\\UI-SilverIcon:0|t "
    end
    out = out .. "|cFFB87333" .. string.format("%d", c) .. "|r|TInterface\\MoneyFrame\\UI-CopperIcon:0|t"
    return out
end
local function BuildUI()
    WeeklyChecklistDB = WeeklyChecklistDB or {}
    WeeklyChecklistDB.sectionCollapsed = WeeklyChecklistDB.sectionCollapsed or {}
    local collapsed = WeeklyChecklistDB.sectionCollapsed
    local scale = iconCfg.fontSize / 11
    ROW_H  = math.floor(22  * scale + 0.5)
    NAME_W = math.floor(184 * scale + 0.5)
    TOON_W = math.floor(88  * scale + 0.5)
    FONT_S  = iconCfg.fontSize
    FONT_XS = math.max(FONT_SIZE_MIN, iconCfg.fontSize - 1)
    local toons = {}
    local currentKey = GetToonKey()
    local currentToon = nil
    local hiddenToons = WeeklyChecklistDB.hiddenToons or {}
    local lastWeeklyReset = GetLastWeeklyReset()
    for key, t in pairs(WeeklyChecklistDB) do
        if type(t)=="table" and t.level and t.level>=MAX_LEVEL then
            t.dbKey = key
            if key == currentKey then
                currentToon = t
            elseif not hiddenToons[key] then
                local hasCurrentData = t.lastUpdate and t.lastUpdate >= lastWeeklyReset
                if not hasCurrentData and t.lastSession then
                    local display = {}
                    for k, v in pairs(t) do display[k] = v end
                    for k, v in pairs(t.lastSession) do
                        if k ~= "time" then display[k] = v end
                    end
                    display.raids               = t.raids
                    display.prey                = t.prey
                    display.worldBossKilled     = t.worldBossKilled
                    display.worldBossName       = t.worldBossName
                    display.mythicPlus          = t.mythicPlus
                    display.delves              = t.delves
                    display.treasureHunterGrace = t.treasureHunterGrace
                    display.keystone            = t.keystone
                    display.lastSessionTime = t.lastSession.time
                    display.isLastSession   = true
                    display.dbKey = key
                    table.insert(toons, display)
                else
                    table.insert(toons, t)
                end
            end
        end
    end
    table.sort(toons, function(a,b) return (a.itemLevel or 0)>(b.itemLevel or 0) end)
    if currentToon then table.insert(toons, 1, currentToon) end
    local ROWS = {}
    table.insert(ROWS, { type="info", label=WCLL.itemLevel, key="itemLevel", cr=C.txtInfo })
    table.insert(ROWS, { type="sep", label=WCLL.sepCurrency })
    table.insert(ROWS, { type="flame",      label=WCLL.flame,      cr=C.txtFlame,   rowKey="flame" })
    local manaVesselCr = {1.00, 0.38, 0.10}
    table.insert(ROWS, { type="manaVessel", label=WCLL.manaVessel, cr=manaVesselCr, rowKey="manaVessel" })
    local voidCoreCr = {0.29, 0.23, 0.71}
    table.insert(ROWS, { type="voidCore",    label=WCLL.voidCore,    cr=voidCoreCr,   rowKey="voidCore" })
    local ascendShardCr = {0.60, 0.80, 1.00}
    table.insert(ROWS, { type="ascendShard", label=WCLL.ascendShard, cr=ascendShardCr, rowKey="ascendShard" })
    local crestColors = {
        [3383] = {1.00, 1.00, 1.00},
        [3341] = {0.30, 1.00, 0.30},
        [3343] = {0.40, 0.70, 1.00},
        [3345] = {0.78, 0.35, 1.00},
        [3347] = {1.00, 0.82, 0.00},
        [3405] = {0.70, 0.13, 0.13},
    }
    for _, c in ipairs(DAWNCREST_CURRENCIES) do
        local shortName = WCLL.crestShort[c.id] or c.short
        local cr = crestColors[c.id] or C.txtCrest
        local label = (c.id == 3405) and shortName or (shortName..WCLL.crestSuffix)
        table.insert(ROWS, { type="crest", label=label, crestID=c.id, cr=cr, rowKey="crest_"..c.id })
    end
    for _, c in ipairs(EXTRA_CURRENCIES) do
        table.insert(ROWS, { type="crest", label=WCLL.extraCurrency[c.id] or tostring(c.id), crestID=c.id, cr=c.cr, rowKey="extra_"..c.id })
    end
    local vaultKeyCr    = {1.00, 0.55, 0.10}
    local keyFragmentCr = {0.28, 0.62, 0.78}
    local dundunCr      = {0.52, 0.72, 0.38}
    table.insert(ROWS, { type="vaultKey",    label=WCLL.vaultKey,    cr=vaultKeyCr,    rowKey="vaultKey" })
    table.insert(ROWS, { type="keyFragment", label=WCLL.keyFragment, cr=keyFragmentCr, rowKey="keyFragment" })
    table.insert(ROWS, { type="dundunShard", label=WCLL.dundunShard, cr=dundunCr,      rowKey="dundunShard" })
    local graceCr = {1.00, 0.98, 0.77}
    table.insert(ROWS, { type="treasureGrace", label=WCLL.treasureHunterGrace, cr=graceCr, rowKey="treasureGrace" })
    local mpCr = {0.40, 0.80, 1.00}
    table.insert(ROWS, { type="sep", label=WCLL.sepMythicPlus })
    table.insert(ROWS, { type="keystone",   label=WCLL.keystone,      cr=mpCr })
    table.insert(ROWS, { type="mythicPlus", label=WCLL.sepMythicPlus, cr=mpCr })
    table.insert(ROWS, { type="sep", label=WCLL.sepRaidMerged })
    for _, r in ipairs(RAIDS) do
        local rLabel = WCLL.raidName[r.name] or r.name
        table.insert(ROWS, { type="raidMerged", label=rLabel, raidName=r.name, def=r.bosses })
    end
    local delveCr = {0.55, 0.82, 0.84}
    table.insert(ROWS, { type="sep", label=WCLL.sepOutdoor })
    table.insert(ROWS, { type="delve",     label=WCLL.sepDelve,   cr=delveCr })
    table.insert(ROWS, { type="worldBoss", cr=C.txtRaidLabel })
    for _, d in ipairs(PREY_DIFFS) do
        local diffLabel = WCLL.preyDiff[d.key] or d.key
        table.insert(ROWS, { type="prey", label=WCLL.sepPrey.." ("..diffLabel..")", preyKey=d.key, def=d.bosses, cr={1.00,0.76,0.57} })
    end
    table.insert(ROWS, { type="sep", label=WCLL.sepMythic })
    for _, d in ipairs(MYTHIC_DUNGEONS) do
        local displayLabel = WCLL.dungName[d.koName] or d.koName
        table.insert(ROWS, { type="dungeon", label=displayLabel, dungName=d.label, def=d.bosses, koName=d.koName })
    end
    local currentTag = nil
    for _, row in ipairs(ROWS) do
        if row.type == "sep" then
            currentTag = row.label
        else
            row.sectionKey = currentTag
        end
    end
    WeeklyChecklistDB.hiddenCurrencyRows = WeeklyChecklistDB.hiddenCurrencyRows or {}
    local hiddenCurrencyRows = WeeklyChecklistDB.hiddenCurrencyRows
    local visibleCount = 0
    local totalRowHeight = 0
    local curTagV = nil
    for _, row in ipairs(ROWS) do
        if row.type == "sep" then
            curTagV = row.label
            visibleCount = visibleCount + 1
            totalRowHeight = totalRowHeight + ROW_H
        elseif not (curTagV and collapsed[curTagV]) then

            if row.rowKey and hiddenCurrencyRows[row.rowKey] then

            else
                visibleCount = visibleCount + 1
                if row.type == "mythicPlus" then
                    totalRowHeight = totalRowHeight + math.floor(ROW_H * 1.55)
                else
                    totalRowHeight = totalRowHeight + ROW_H
                end
            end
        end
    end
    local nT     = #toons
    local totalW = PAD_L*2 + NAME_W + nT*TOON_W
    local totalH = PAD_T + totalRowHeight + 28
    if not mainFrame then
        mainFrame = CreateFrame("Frame","WeeklyChecklistFrame",UIParent)
        table.insert(UISpecialFrames, "WeeklyChecklistFrame")
        mainFrame:SetFrameStrata("HIGH"); mainFrame:SetMovable(true); mainFrame:EnableMouse(true)
        mainFrame:RegisterForDrag("LeftButton")
        mainFrame:SetScript("OnHide", function() if wcToonMenu then wcToonMenu:Hide() end end)
        mainFrame:SetScript("OnDragStart", mainFrame.StartMoving)
        mainFrame:SetScript("OnDragStop", function(self)
            self:StopMovingOrSizing()
            local x, y = self:GetLeft(), self:GetTop()
            WeeklyChecklistDB.winX = x
            WeeklyChecklistDB.winY = y
        end)
        local wx = WeeklyChecklistDB.winX
        local wy = WeeklyChecklistDB.winY
        if wx and wy then
            mainFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", wx, wy)
        else
            mainFrame:SetPoint("CENTER")
        end
        local bd = CreateFrame("Frame", nil, mainFrame, "BackdropTemplate")
        bd:SetAllPoints()
        bd:SetBackdrop({
            bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
            edgeFile = "Interface\\ChatFrame\\ChatFrameBackground",
            edgeSize = 1,
            insets   = {left=1,right=1,top=1,bottom=1},
        })
        bd:SetBackdropColor(unpack(C.bg))
        bd:SetBackdropBorderColor(0.18, 0.18, 0.20, 1.0)
        mainFrame.bd = bd
        local titleBar = CreateFrame("Frame", nil, mainFrame)
        titleBar:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", 0, 0)
        titleBar:SetPoint("TOPRIGHT", mainFrame, "TOPRIGHT", 0, 0)
        titleBar:SetHeight(26)
        local titleBg = titleBar:CreateTexture(nil,"BACKGROUND")
        titleBg:SetColorTexture(0.06,0.06,0.08,1); titleBg:SetAllPoints()
        mainFrame.titleTxt = titleBar:CreateFontString(nil,"OVERLAY")
        mainFrame.titleTxt:SetFont(FF(12)); mainFrame.titleTxt:SetText("[周常]账号信息")
        mainFrame.titleTxt:SetTextColor(0.92,0.92,0.95); mainFrame.titleTxt:SetJustifyH("CENTER")
        mainFrame.titleTxt:SetPoint("CENTER", titleBar, "CENTER")
        local close = CreateFrame("Button", nil, mainFrame)
        close:SetSize(20,20); close:SetPoint("TOPRIGHT", mainFrame, "TOPRIGHT", -4, -4)
        local closeTxt = close:CreateFontString(nil,"OVERLAY")
        closeTxt:SetFont(FF(14)); closeTxt:SetText("x")
        closeTxt:SetTextColor(0.7,0.7,0.7); closeTxt:SetAllPoints()
        close:SetScript("OnClick",  function() if wcToonMenu then wcToonMenu:Hide() end; mainFrame:Hide() end)
        close:SetScript("OnEnter",  function() closeTxt:SetTextColor(1,0.3,0.3) end)
        close:SetScript("OnLeave",  function() closeTxt:SetTextColor(0.7,0.7,0.7) end)
        local vaultBtn = CreateFrame("Button", nil, titleBar)
        vaultBtn:SetSize(22, 22)
        vaultBtn:SetPoint("LEFT", mainFrame.titleTxt, "RIGHT", 6, 0)
        local vaultTex = vaultBtn:CreateTexture(nil, "ARTWORK")
        vaultTex:SetAtlas("UI-Journeys-GreatVault-Button")
        vaultTex:SetAllPoints()
        vaultTex:SetAlpha(0.85)
        vaultBtn:SetScript("OnEnter", function()
            vaultTex:SetAlpha(1.0)
            GameTooltip:SetOwner(vaultBtn, "ANCHOR_BOTTOM")
            GameTooltip:SetText(useEnglish and "Open Great Vault" or "打开宏伟宝库", 1, 1, 1)
            GameTooltip:Show()
        end)
        vaultBtn:SetScript("OnLeave", function() vaultTex:SetAlpha(0.85); GameTooltip:Hide() end)
        vaultBtn:SetScript("OnClick", function()
            C_AddOns.LoadAddOn("Blizzard_WeeklyRewards")
            if WeeklyRewardsFrame then
                if WeeklyRewardsFrame:IsShown() then
                    WeeklyRewardsFrame:Hide()
                    mainFrame:SetFrameStrata("HIGH")
                else
                    WeeklyRewardsFrame:SetFrameStrata("DIALOG")
                    mainFrame:SetFrameStrata("HIGH")
                    WeeklyRewardsFrame:Show()
                end
            end
        end)
        local langBtn = CreateFrame("Button", nil, titleBar)
        langBtn:SetSize(28,16)
        langBtn:SetPoint("LEFT", titleBar, "LEFT", 6, 0)
        local langBg = langBtn:CreateTexture(nil,"BACKGROUND")
        langBg:SetColorTexture(0.18,0.16,0.26,1); langBg:SetAllPoints()
        local langTxt = langBtn:CreateFontString(nil,"OVERLAY")
        langTxt:SetFont(FF(9)); langTxt:SetText(WCLL.btnLang)
        langTxt:SetTextColor(0.80,0.74,0.50); langTxt:SetJustifyH("CENTER"); langTxt:SetAllPoints()
        langBtn:SetScript("OnEnter", function()
            langTxt:SetTextColor(1,0.95,0.40); langBg:SetColorTexture(0.28,0.24,0.40,1) end)
        langBtn:SetScript("OnLeave", function()
            langTxt:SetTextColor(0.80,0.74,0.50); langBg:SetColorTexture(0.18,0.16,0.26,1) end)
        langBtn:SetScript("OnClick", function()
            SetLocale(not useEnglish)
            langTxt:SetText(WCLL.btnLang)
            WeeklyChecklistDB.useEnglish = useEnglish and 1 or 0
            BuildUI()
        end)
        mainFrame.langBtnTxt = langTxt

        local toolbar = CreateFrame("Frame", nil, mainFrame)
        toolbar:SetPoint("TOPLEFT",  mainFrame, "TOPLEFT",  0, -26)
        toolbar:SetPoint("TOPRIGHT", mainFrame, "TOPRIGHT", 0, -26)
        toolbar:SetHeight(18)
        local tbBg = toolbar:CreateTexture(nil, "BACKGROUND")
        tbBg:SetColorTexture(0.08,0.08,0.11,1); tbBg:SetAllPoints()
        local tbLine = toolbar:CreateTexture(nil, "ARTWORK")
        tbLine:SetColorTexture(0.14,0.14,0.18,1); tbLine:SetHeight(1)
        tbLine:SetPoint("BOTTOMLEFT",toolbar,"BOTTOMLEFT"); tbLine:SetPoint("BOTTOMRIGHT",toolbar,"BOTTOMRIGHT")
        local fontBtn = CreateFrame("Button", nil, toolbar)
        fontBtn:SetSize(96, 12)
        fontBtn:SetPoint("TOPLEFT", toolbar, "TOPLEFT", 6, -3)
        local fontBg = fontBtn:CreateTexture(nil, "BACKGROUND")
        fontBg:SetColorTexture(0.14,0.12,0.22,1); fontBg:SetAllPoints()
        local fontBorder = fontBtn:CreateTexture(nil, "BORDER")
        fontBorder:SetColorTexture(0.28,0.22,0.50,1)
        fontBorder:SetPoint("TOPLEFT",fontBtn,"TOPLEFT",0,0)
        fontBorder:SetPoint("BOTTOMRIGHT",fontBtn,"BOTTOMRIGHT",0,-1)
        local fontBorderInner = fontBtn:CreateTexture(nil, "ARTWORK")
        fontBorderInner:SetColorTexture(0.14,0.12,0.22,1)
        fontBorderInner:SetPoint("TOPLEFT",fontBtn,"TOPLEFT",1,0)
        fontBorderInner:SetPoint("BOTTOMRIGHT",fontBtn,"BOTTOMRIGHT",-1,-1)
        local fontTx = fontBtn:CreateFontString(nil, "OVERLAY")
        fontTx:SetFont(FF(8)); fontTx:SetTextColor(0.80,0.74,0.90)
        fontTx:SetJustifyH("CENTER"); fontTx:SetAllPoints()
        local function RefreshFontLabel()
            local e = FONT_LIST[iconCfg.fontIndex]
            local raw = (e and e.name) or "字体"
            local count, bytes = 0, 0
            for pos = 1, #raw do
                local b = raw:byte(pos)
                if b < 0x80 or b >= 0xC0 then
                    count = count + 1
                    if count > 10 then
                        raw = raw:sub(1, bytes) .. ".."
                        break
                    end
                end
                bytes = pos
            end
            fontTx:SetText(raw)
        end
        RefreshFontLabel()
        local sep = toolbar:CreateTexture(nil, "ARTWORK")
        sep:SetColorTexture(0.20,0.18,0.28,1); sep:SetWidth(1); sep:SetHeight(12)
        sep:SetPoint("LEFT", fontBtn, "RIGHT", 4, 0)
        sep:SetPoint("TOP", toolbar, "TOP", 0, -3)
        local function MakeSzBtn(label, xRef, xOff, delta)
            local b = CreateFrame("Button", nil, toolbar)
            b:SetSize(13, 12)
            b:SetPoint("LEFT", xRef, "RIGHT", xOff, 0)
            b:SetPoint("TOP", toolbar, "TOP", 0, -3)
            local bg = b:CreateTexture(nil, "BACKGROUND")
            bg:SetColorTexture(0.14,0.12,0.22,1); bg:SetAllPoints()
            local tx = b:CreateFontString(nil, "OVERLAY")
            tx:SetFont(FF(10)); tx:SetText(label)
            tx:SetTextColor(0.70,0.88,0.55); tx:SetJustifyH("CENTER"); tx:SetAllPoints()
            b:SetScript("OnEnter", function() bg:SetColorTexture(0.22,0.20,0.34,1) end)
            b:SetScript("OnLeave", function() bg:SetColorTexture(0.14,0.12,0.22,1) end)
            b:SetScript("OnClick", function()
                iconCfg.fontSize = math.max(FONT_SIZE_MIN, math.min(FONT_SIZE_MAX, iconCfg.fontSize + delta))
                FONT_S  = iconCfg.fontSize
                FONT_XS = math.max(FONT_SIZE_MIN, iconCfg.fontSize - 1)
                SaveIconCfg(); BuildUI()
            end)
            return b
        end
        local btnMinus = MakeSzBtn("-", sep, 4, -1)
        local szDisp = toolbar:CreateFontString(nil, "OVERLAY")
        szDisp:SetFont(FF(8)); szDisp:SetTextColor(0.65,0.65,0.75)
        szDisp:SetJustifyH("CENTER")
        szDisp:SetPoint("LEFT", btnMinus, "RIGHT", 3, 0)
        szDisp:SetPoint("TOP", toolbar, "TOP", 0, -3)
        szDisp:SetSize(26, 12)
        szDisp:SetText(iconCfg.fontSize.."px")
        local plusBtn = MakeSzBtn("+", szDisp, 3, 1)

        local partyKeyBtn = CreateFrame("Button", nil, toolbar)
        partyKeyBtn:SetSize(76, 12)
        partyKeyBtn:SetPoint("LEFT", plusBtn, "RIGHT", 3, 0)
        local partyKeyBg = partyKeyBtn:CreateTexture(nil, "BACKGROUND")
        partyKeyBg:SetColorTexture(0.14,0.12,0.22,1); partyKeyBg:SetAllPoints()
        local partyKeyBorder = partyKeyBtn:CreateTexture(nil, "BORDER")
        partyKeyBorder:SetColorTexture(0.28,0.22,0.50,1)
        partyKeyBorder:SetPoint("TOPLEFT",partyKeyBtn,"TOPLEFT",0,0)
        partyKeyBorder:SetPoint("BOTTOMRIGHT",partyKeyBtn,"BOTTOMRIGHT",0,-1)
        local partyKeyInner = partyKeyBtn:CreateTexture(nil, "ARTWORK")
        partyKeyInner:SetColorTexture(0.14,0.12,0.22,1)
        partyKeyInner:SetPoint("TOPLEFT",partyKeyBtn,"TOPLEFT",1,0)
        partyKeyInner:SetPoint("BOTTOMRIGHT",partyKeyBtn,"BOTTOMRIGHT",-1,-1)
        local partyKeyTx = partyKeyBtn:CreateFontString(nil, "OVERLAY")
        partyKeyTx:SetFont(FF(8)); partyKeyTx:SetJustifyH("CENTER"); partyKeyTx:SetAllPoints()
        local function partyKeyRefreshLabel()
            if WeeklyChecklistDB and WeeklyChecklistDB.ktShown then
                partyKeyTx:SetText("|cFFCCBDE6队伍钥石|r |cFF66E666on|r")
            else
                partyKeyTx:SetText("|cFFCCBDE6队伍钥石|r |cFFE64D4Doff|r")
            end
        end
        partyKeyRefreshLabel()
        partyKeyBtn:SetScript("OnEnter", function() partyKeyBg:SetColorTexture(0.20,0.18,0.32,1) end)
        partyKeyBtn:SetScript("OnLeave", function() partyKeyBg:SetColorTexture(0.14,0.12,0.22,1) end)
        partyKeyBtn:SetScript("OnClick", function()
            kt_toggle()
            partyKeyRefreshLabel()
        end)

        local enterKeyBtn = CreateFrame("Button", nil, toolbar)
        enterKeyBtn:SetSize(80, 12)
        enterKeyBtn:SetPoint("LEFT", partyKeyBtn, "RIGHT", 3, 0)
        enterKeyBtn:SetPoint("BOTTOM", partyKeyBtn, "BOTTOM", 0, 0)
        local enterKeyBg = enterKeyBtn:CreateTexture(nil, "BACKGROUND")
        enterKeyBg:SetColorTexture(0.12,0.14,0.22,1); enterKeyBg:SetAllPoints()
        local enterKeyBorder = enterKeyBtn:CreateTexture(nil, "BORDER")
        enterKeyBorder:SetColorTexture(0.22,0.28,0.50,1)
        enterKeyBorder:SetPoint("TOPLEFT",enterKeyBtn,"TOPLEFT",0,0)
        enterKeyBorder:SetPoint("BOTTOMRIGHT",enterKeyBtn,"BOTTOMRIGHT",0,-1)
        local enterKeyInner = enterKeyBtn:CreateTexture(nil, "ARTWORK")
        enterKeyInner:SetColorTexture(0.12,0.14,0.22,1)
        enterKeyInner:SetPoint("TOPLEFT",enterKeyBtn,"TOPLEFT",1,0)
        enterKeyInner:SetPoint("BOTTOMRIGHT",enterKeyBtn,"BOTTOMRIGHT",-1,-1)
        local enterKeyTx = enterKeyBtn:CreateFontString(nil, "OVERLAY")
        enterKeyTx:SetFont(FF(8)); enterKeyTx:SetJustifyH("CENTER"); enterKeyTx:SetAllPoints()
        local function enterKeyRefreshLabel()
            if WeeklyChecklistDB and WeeklyChecklistDB.enterKeyShown then
                enterKeyTx:SetText("|cFFBDE6CC秘境钥石|r |cFF66E666on|r")
            else
                enterKeyTx:SetText("|cFFBDE6CC秘境钥石|r |cFFE64D4Doff|r")
            end
        end
        enterKeyRefreshLabel()
        enterKeyBtn:SetScript("OnEnter", function() enterKeyBg:SetColorTexture(0.18,0.20,0.34,1) end)
        enterKeyBtn:SetScript("OnLeave", function() enterKeyBg:SetColorTexture(0.12,0.14,0.22,1) end)
        enterKeyBtn:SetScript("OnClick", function()
            WeeklyChecklistDB = WeeklyChecklistDB or {}
            WeeklyChecklistDB.enterKeyShown = not WeeklyChecklistDB.enterKeyShown
            enterKeyRefreshLabel()
            if kt_frame and WeeklyChecklistDB.ktShown then
                kt_refresh()
            end
        end)

        --[[local jumpSoundBtn = CreateFrame("Button", nil, toolbar)
        jumpSoundBtn:SetSize(76, 12)
        jumpSoundBtn:SetPoint("LEFT", enterKeyBtn, "RIGHT", 3, 0)
        local jumpSoundBg = jumpSoundBtn:CreateTexture(nil, "BACKGROUND")
        jumpSoundBg:SetColorTexture(0.12,0.14,0.18,1); jumpSoundBg:SetAllPoints()
        local jumpSoundBorder = jumpSoundBtn:CreateTexture(nil, "BORDER")
        jumpSoundBorder:SetColorTexture(0.24,0.28,0.36,1)
        jumpSoundBorder:SetPoint("TOPLEFT",jumpSoundBtn,"TOPLEFT",0,0)
        jumpSoundBorder:SetPoint("BOTTOMRIGHT",jumpSoundBtn,"BOTTOMRIGHT",0,-1)
        local jumpSoundInner = jumpSoundBtn:CreateTexture(nil, "ARTWORK")
        jumpSoundInner:SetColorTexture(0.12,0.14,0.18,1)
        jumpSoundInner:SetPoint("TOPLEFT",jumpSoundBtn,"TOPLEFT",1,0)
        jumpSoundInner:SetPoint("BOTTOMRIGHT",jumpSoundBtn,"BOTTOMRIGHT",-1,-1)
        local jumpSoundTx = jumpSoundBtn:CreateFontString(nil, "OVERLAY")
        jumpSoundTx:SetFont(FF(8)); jumpSoundTx:SetJustifyH("CENTER"); jumpSoundTx:SetAllPoints()
        local function jumpSoundRefreshLabel()
            if WeeklyChecklistDB and WeeklyChecklistDB.jumpSound then
                jumpSoundTx:SetText("|cFFE6D5BD跳起音效|r |cFF66E666on|r")
            else
                jumpSoundTx:SetText("|cFFE6D5BD跳起音效|r |cFFE64D4Doff|r")
            end
        end
        jumpSoundRefreshLabel()
        jumpSoundBtn:SetScript("OnEnter", function() jumpSoundBg:SetColorTexture(0.18,0.20,0.26,1) end)
        jumpSoundBtn:SetScript("OnLeave", function() jumpSoundBg:SetColorTexture(0.12,0.14,0.18,1) end)
        jumpSoundBtn:SetScript("OnClick", function()
            WeeklyChecklistDB = WeeklyChecklistDB or {}
            WeeklyChecklistDB.jumpSound = not WeeklyChecklistDB.jumpSound
            jumpSoundRefreshLabel()
        end)]]

        local fontPanel = CreateFrame("Frame", nil, UIParent)
        fontPanel:SetFrameStrata("DIALOG")
        fontPanel:SetWidth(160)
        fontPanel:Hide()
        local panelBd = CreateFrame("Frame", nil, fontPanel, "BackdropTemplate")
        panelBd:SetAllPoints()
        panelBd:SetBackdrop({
            bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
            edgeFile = "Interface\\ChatFrame\\ChatFrameBackground",
            edgeSize = 1, insets = {left=1,right=1,top=1,bottom=1},
        })
        panelBd:SetBackdropColor(0.06,0.06,0.10,0.97)
        panelBd:SetBackdropBorderColor(0.28,0.22,0.50,1)
        local function PopulateFontPanel()
            for _, c in ipairs({fontPanel:GetChildren()}) do
                if c.wcFontItem then c:Hide(); c:SetParent(nil) end
            end
            local ITEM_H = 18
            local maxShow = math.min(#FONT_LIST, 14)
            fontPanel:SetHeight(maxShow * ITEM_H + 2)
            for i = 1, maxShow do
                local entry = FONT_LIST[i]
                local item = CreateFrame("Button", nil, fontPanel)
                item.wcFontItem = true
                item:SetSize(156, ITEM_H)
                item:SetPoint("TOPLEFT", fontPanel, "TOPLEFT", 2, -(i-1)*ITEM_H - 1)
                local ibg = item:CreateTexture(nil, "BACKGROUND")
                ibg:SetColorTexture(0,0,0,0); ibg:SetAllPoints()
                local itx = item:CreateFontString(nil, "OVERLAY")
                local ok = pcall(function() itx:SetFont(entry.file, 9) end)
                if not ok then itx:SetFont(FF(9)) end
                local isSel = (i == iconCfg.fontIndex)
                itx:SetTextColor(isSel and 1 or 0.75, isSel and 0.88 or 0.72, isSel and 0.30 or 0.88)
                itx:SetText(entry.name); itx:SetJustifyH("LEFT"); itx:SetAllPoints()
                local ci = i
                item:SetScript("OnEnter", function() ibg:SetColorTexture(0.22,0.18,0.36,0.8) end)
                item:SetScript("OnLeave", function() ibg:SetColorTexture(0,0,0,0) end)
                item:SetScript("OnClick", function()
                    iconCfg.fontIndex = ci; SaveIconCfg()
                    fontPanel:Hide(); BuildUI()
                end)
            end
        end
        fontBtn:SetScript("OnEnter", function() fontBg:SetColorTexture(0.20,0.18,0.32,1) end)
        fontBtn:SetScript("OnLeave", function() fontBg:SetColorTexture(0.14,0.12,0.22,1) end)
        fontBtn:SetScript("OnClick", function()
            if fontPanel:IsShown() then
                fontPanel:Hide()
            else
                PopulateFontPanel()
                fontPanel:ClearAllPoints()
                fontPanel:SetPoint("TOPLEFT", fontBtn, "BOTTOMLEFT", 0, -2)
                fontPanel:Show()
            end
        end)
        local fontClose = CreateFrame("Frame", nil, UIParent)
        fontClose:SetAllPoints(); fontClose:SetFrameStrata("HIGH")
        fontClose:EnableMouse(true); fontClose:Hide()
        fontClose:SetScript("OnMouseDown", function() fontPanel:Hide(); fontClose:Hide() end)
        fontPanel:SetScript("OnShow", function() fontClose:Show() end)
        fontPanel:SetScript("OnHide", function() fontClose:Hide() end)
        mainFrame.toolbar    = toolbar
        mainFrame.fontTx     = fontTx
        mainFrame.szDisp     = szDisp
        mainFrame.refreshFL  = RefreshFontLabel
    else
        if mainFrame.langBtnTxt then mainFrame.langBtnTxt:SetText(WCLL.btnLang) end
        if mainFrame.refreshFL  then mainFrame.refreshFL() end
        if mainFrame.szDisp     then mainFrame.szDisp:SetText(iconCfg.fontSize.."px") end

    end
    local sh = UIParent:GetHeight()
    mainFrame:SetSize(totalW, totalH)
    mainFrame.bd:SetSize(totalW, totalH)
    if mainFrame.toolbar then mainFrame.toolbar:SetWidth(totalW) end
    local wx = WeeklyChecklistDB.winX
    local wy = WeeklyChecklistDB.winY
    if wx and wy then
        local sw = UIParent:GetWidth()
        if wx + totalW > sw then wx = sw - totalW end
        if wx < 0 then wx = 0 end
        if wy > sh then wy = sh end
        if wy - totalH < 0 then wy = totalH end
        mainFrame:ClearAllPoints()
        mainFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", wx, wy)
    end
    for _, child in ipairs({ mainFrame:GetChildren() }) do
        if child.wcManaged then child:Hide(); child:SetParent(nil) end
    end
    local bX = PAD_L
    local bY = -(PAD_T)
    local hdr = CreateFrame("Frame", nil, mainFrame)
    hdr.wcManaged = true
    hdr:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", bX, bY)
    hdr:SetSize(totalW-PAD_L*2, ROW_H)
    local hdrBg = hdr:CreateTexture(nil,"BACKGROUND")
    hdrBg:SetColorTexture(unpack(C.header)); hdrBg:SetAllPoints()
    WeeklyChecklistDB = WeeklyChecklistDB or {}
    local equipInfoBtn = CreateFrame("Button", nil, hdr)
    equipInfoBtn:SetPoint("TOPLEFT", hdr, "TOPLEFT", 2, -2)
    equipInfoBtn:SetSize(8, 12)
    --equipInfoBtn:SetSize(58, 12)
    --[[local eiBg = equipInfoBtn:CreateTexture(nil, "BACKGROUND")
    eiBg:SetColorTexture(0.14, 0.12, 0.22, 1); eiBg:SetAllPoints()
    local eiBorder = equipInfoBtn:CreateTexture(nil, "BORDER")
    eiBorder:SetColorTexture(0.28, 0.22, 0.50, 1)
    eiBorder:SetPoint("TOPLEFT", equipInfoBtn, "TOPLEFT", 0, 0)
    eiBorder:SetPoint("BOTTOMRIGHT", equipInfoBtn, "BOTTOMRIGHT", 0, -1)
    local eiInner = equipInfoBtn:CreateTexture(nil, "ARTWORK")
    eiInner:SetColorTexture(0.14, 0.12, 0.22, 1)
    eiInner:SetPoint("TOPLEFT", equipInfoBtn, "TOPLEFT", 1, 0)
    eiInner:SetPoint("BOTTOMRIGHT", equipInfoBtn, "BOTTOMRIGHT", -1, -1)
    local eiTx = equipInfoBtn:CreateFontString(nil, "OVERLAY")
    eiTx:SetFont(FF(8)); eiTx:SetJustifyH("CENTER"); eiTx:SetAllPoints()
    local function WC_RefreshEquipBtn()
        if WeeklyChecklistDB.showEquipInfo then
            eiTx:SetText("|cFFCCBDE6装备信息|r |cFF66E666on|r")
        else
            eiTx:SetText("|cFFCCBDE6装备信息|r |cFFE64D4Doff|r")
        end
    end
    WC_RefreshEquipBtn()
    equipInfoBtn:SetScript("OnEnter", function() eiBg:SetColorTexture(0.20, 0.18, 0.32, 1) end)
    equipInfoBtn:SetScript("OnLeave", function() eiBg:SetColorTexture(0.14, 0.12, 0.22, 1) end)
    equipInfoBtn:SetScript("OnClick", function()
        WeeklyChecklistDB.showEquipInfo = not WeeklyChecklistDB.showEquipInfo
        WC_RefreshEquipBtn()
        if WeeklyChecklistDB.showEquipInfo then
            C_Timer.After(0.05, WC_EquipOverlay_Update)
            C_Timer.After(0.05, WC_AvgIlvl_Update)
        else
            WC_EquipOverlay_Hide()
            if PaperDollFrame_UpdateStats and CharacterFrame and CharacterFrame:IsShown() then
                PaperDollFrame_UpdateStats()
            end
        end
    end)]]
    local function MakeHdrBtn(parent, w, anchorRef, anchorSide)
        local btn = CreateFrame("Button", nil, parent)
        btn:SetSize(w, 12)
        btn:SetPoint("LEFT", anchorRef, anchorSide, 3, 0)
        local bg = btn:CreateTexture(nil, "BACKGROUND")
        bg:SetColorTexture(0.14,0.12,0.22,1); bg:SetAllPoints()
        local bdr = btn:CreateTexture(nil, "BORDER")
        bdr:SetColorTexture(0.28,0.22,0.50,1)
        bdr:SetPoint("TOPLEFT",btn,"TOPLEFT",0,0)
        bdr:SetPoint("BOTTOMRIGHT",btn,"BOTTOMRIGHT",0,-1)
        local inn = btn:CreateTexture(nil, "ARTWORK")
        inn:SetColorTexture(0.14,0.12,0.22,1)
        inn:SetPoint("TOPLEFT",btn,"TOPLEFT",1,0)
        inn:SetPoint("BOTTOMRIGHT",btn,"BOTTOMRIGHT",-1,-1)
        local tx = btn:CreateFontString(nil,"OVERLAY")
        tx:SetFont(FF(8)); tx:SetJustifyH("CENTER"); tx:SetAllPoints()
        btn:SetScript("OnEnter", function() bg:SetColorTexture(0.20,0.18,0.32,1) end)
        btn:SetScript("OnLeave", function() bg:SetColorTexture(0.14,0.12,0.22,1) end)
        btn._tx = tx; btn._bg = bg
        return btn
    end
    local currHideBtn = MakeHdrBtn(hdr, 60, equipInfoBtn, "RIGHT")
    local currHideBtnTx = currHideBtn._tx
    local function RefreshCurrHideLabel()
        local count = 0
        local hcr = WeeklyChecklistDB.hiddenCurrencyRows or {}
        for _ in pairs(hcr) do count = count + 1 end
        if count > 0 then
            currHideBtnTx:SetText(WCLL.btnHideCurrency.."("..count..")")
            currHideBtnTx:SetTextColor(1.00,0.60,0.40)
        else
            currHideBtnTx:SetText(WCLL.btnHideCurrency)
            currHideBtnTx:SetTextColor(0.50,0.48,0.55)
        end
    end
    RefreshCurrHideLabel()
    local currHidePanel = CreateFrame("Frame", nil, UIParent)
    currHidePanel:SetFrameStrata("DIALOG")
    currHidePanel:SetWidth(230)
    currHidePanel:Hide()
    local currHidePanelBd = CreateFrame("Frame", nil, currHidePanel, "BackdropTemplate")
    currHidePanelBd:SetAllPoints()
    currHidePanelBd:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\ChatFrame\\ChatFrameBackground",
        edgeSize = 1, insets = {left=1,right=1,top=1,bottom=1},
    })
    currHidePanelBd:SetBackdropColor(0.06,0.06,0.10,0.97)
    currHidePanelBd:SetBackdropBorderColor(0.28,0.22,0.50,1)
    local function PopulateCurrHidePanel()
        for _, c in ipairs({currHidePanel:GetChildren()}) do
            if c.wcCurrHideItem then c:Hide(); c:SetParent(nil) end
        end
        local hcr = WeeklyChecklistDB.hiddenCurrencyRows or {}
        local keys = {}
        for k in pairs(hcr) do table.insert(keys, k) end
        local ITEM_H = 22
        if #keys == 0 then
            currHidePanel:SetHeight(ITEM_H + 4)
            local empty = CreateFrame("Frame", nil, currHidePanel)
            empty.wcCurrHideItem = true
            empty:SetSize(226, ITEM_H)
            empty:SetPoint("TOPLEFT", currHidePanel, "TOPLEFT", 2, -2)
            local etx = empty:CreateFontString(nil,"OVERLAY")
            etx:SetFont(FF(9)); etx:SetTextColor(0.5,0.5,0.5)
            etx:SetText(WCLL.hideCurrPanelEmpty); etx:SetJustifyH("CENTER"); etx:SetAllPoints()
        else
            currHidePanel:SetHeight(#keys * ITEM_H + 4)
            for i, key in ipairs(keys) do
                local labelStr = hcr[key] or key
                local item = CreateFrame("Button", nil, currHidePanel)
                item.wcCurrHideItem = true
                item:SetSize(226, ITEM_H)
                item:SetPoint("TOPLEFT", currHidePanel, "TOPLEFT", 2, -(i-1)*ITEM_H - 2)
                local ibg = item:CreateTexture(nil,"BACKGROUND")
                ibg:SetColorTexture(0,0,0,0); ibg:SetAllPoints()
                local itx = item:CreateFontString(nil,"OVERLAY")
                itx:SetFont(FF(9)); itx:SetTextColor(0.85,0.85,0.85)
                itx:SetText(labelStr.." |cFF44FF44["..WCLL.hideRestore.."]|r")
                itx:SetJustifyH("LEFT")
                itx:SetPoint("LEFT", item, "LEFT", 4, 0)
                itx:SetSize(222, ITEM_H)
                local capturedKey = key
                item:SetScript("OnEnter", function() ibg:SetColorTexture(0.22,0.18,0.36,0.8) end)
                item:SetScript("OnLeave", function() ibg:SetColorTexture(0,0,0,0) end)
                item:SetScript("OnClick", function()
                    WeeklyChecklistDB.hiddenCurrencyRows[capturedKey] = nil
                    currHidePanel:Hide()
                    BuildUI()
                end)
            end
        end
    end
    currHideBtn:SetScript("OnClick", function()
        if currHidePanel:IsShown() then
            currHidePanel:Hide()
        else
            PopulateCurrHidePanel()
            currHidePanel:ClearAllPoints()
            currHidePanel:SetPoint("TOPLEFT", currHideBtn, "BOTTOMLEFT", 0, -2)
            currHidePanel:Show()
        end
    end)
    local currHidePanelClose = CreateFrame("Frame", nil, UIParent)
    currHidePanelClose:SetAllPoints(); currHidePanelClose:SetFrameStrata("HIGH")
    currHidePanelClose:EnableMouse(true); currHidePanelClose:Hide()
    currHidePanelClose:SetScript("OnMouseDown", function() currHidePanel:Hide(); currHidePanelClose:Hide() end)
    currHidePanel:SetScript("OnShow", function() currHidePanelClose:Show() end)
    currHidePanel:SetScript("OnHide", function() currHidePanelClose:Hide() end)
    local hideBtn = MakeHdrBtn(hdr, 64, currHideBtn, "RIGHT")
    local hideBtnTx = hideBtn._tx
    local function RefreshHideLabel()
        local count = 0
        local ht = WeeklyChecklistDB.hiddenToons or {}
        for _ in pairs(ht) do count = count + 1 end
        if count > 0 then
            hideBtnTx:SetText(WCLL.btnHideMgr.."("..count..")")
            hideBtnTx:SetTextColor(1.00,0.60,0.40)
        else
            hideBtnTx:SetText(WCLL.btnHideMgr)
            hideBtnTx:SetTextColor(0.50,0.48,0.55)
        end
    end
    RefreshHideLabel()
    local hidePanel = CreateFrame("Frame", nil, UIParent)
    hidePanel:SetFrameStrata("DIALOG")
    hidePanel:SetWidth(210)
    hidePanel:Hide()
    local hidePanelBd = CreateFrame("Frame", nil, hidePanel, "BackdropTemplate")
    hidePanelBd:SetAllPoints()
    hidePanelBd:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\ChatFrame\\ChatFrameBackground",
        edgeSize = 1, insets = {left=1,right=1,top=1,bottom=1},
    })
    hidePanelBd:SetBackdropColor(0.06,0.06,0.10,0.97)
    hidePanelBd:SetBackdropBorderColor(0.28,0.22,0.50,1)
    local function PopulateHidePanel()
        for _, c in ipairs({hidePanel:GetChildren()}) do
            if c.wcHideItem then c:Hide(); c:SetParent(nil) end
        end
        local ht = WeeklyChecklistDB.hiddenToons or {}
        local keys = {}
        for k in pairs(ht) do table.insert(keys, k) end
        local ITEM_H = 22
        if #keys == 0 then
            hidePanel:SetHeight(ITEM_H + 4)
            local empty = CreateFrame("Frame", nil, hidePanel)
            empty.wcHideItem = true
            empty:SetSize(206, ITEM_H)
            empty:SetPoint("TOPLEFT", hidePanel, "TOPLEFT", 2, -2)
            local etx = empty:CreateFontString(nil,"OVERLAY")
            etx:SetFont(FF(9)); etx:SetTextColor(0.5,0.5,0.5)
            etx:SetText(WCLL.hidePanelEmpty); etx:SetJustifyH("CENTER"); etx:SetAllPoints()
        else
            hidePanel:SetHeight(#keys * ITEM_H + 4)
            for i, key in ipairs(keys) do
                local tData = WeeklyChecklistDB[key]
                local displayName = (tData and tData.name) or key
                local item = CreateFrame("Button", nil, hidePanel)
                item.wcHideItem = true
                item:SetSize(206, ITEM_H)
                item:SetPoint("TOPLEFT", hidePanel, "TOPLEFT", 2, -(i-1)*ITEM_H - 2)
                local ibg = item:CreateTexture(nil,"BACKGROUND")
                ibg:SetColorTexture(0,0,0,0); ibg:SetAllPoints()
                local itx = item:CreateFontString(nil,"OVERLAY")
                itx:SetFont(FF(9)); itx:SetTextColor(0.85,0.85,0.85)
                itx:SetText(displayName.." |cFF44FF44["..WCLL.hideRestore.."]|r")
                itx:SetJustifyH("LEFT")
                itx:SetPoint("LEFT", item, "LEFT", 4, 0)
                itx:SetSize(202, ITEM_H)
                local capturedKey = key
                item:SetScript("OnEnter", function() ibg:SetColorTexture(0.22,0.18,0.36,0.8) end)
                item:SetScript("OnLeave", function() ibg:SetColorTexture(0,0,0,0) end)
                item:SetScript("OnClick", function()
                    WeeklyChecklistDB.hiddenToons[capturedKey] = nil
                    hidePanel:Hide()
                    BuildUI()
                end)
            end
        end
    end
    hideBtn:SetScript("OnClick", function()
        if hidePanel:IsShown() then
            hidePanel:Hide()
        else
            PopulateHidePanel()
            hidePanel:ClearAllPoints()
            hidePanel:SetPoint("TOPLEFT", hideBtn, "BOTTOMLEFT", 0, -2)
            hidePanel:Show()
        end
    end)
    local hidePanelClose = CreateFrame("Frame", nil, UIParent)
    hidePanelClose:SetAllPoints(); hidePanelClose:SetFrameStrata("HIGH")
    hidePanelClose:EnableMouse(true); hidePanelClose:Hide()
    hidePanelClose:SetScript("OnMouseDown", function() hidePanel:Hide(); hidePanelClose:Hide() end)
    hidePanel:SetScript("OnShow", function() hidePanelClose:Show() end)
    hidePanel:SetScript("OnHide", function() hidePanelClose:Hide() end)
    for col, t in ipairs(toons) do
        local r,g,b = GetCC(t.class)
        local nameBtn = CreateFrame("Button", nil, hdr)
        nameBtn:SetPoint("TOPLEFT", hdr, "TOPLEFT", NAME_W+(col-1)*TOON_W, 0)
        nameBtn:SetSize(TOON_W, ROW_H)
        nameBtn:EnableMouse(true)
        nameBtn:RegisterForClicks("LeftButtonUp","RightButtonUp")
        local nm = nameBtn:CreateFontString(nil,"OVERLAY")
        nm:SetFont(FF(FONT_S)); nm:SetText(t.name or "?")
        nm:SetTextColor(r,g,b); nm:SetJustifyH("CENTER"); nm:SetAllPoints()
        local capturedT = t
        nameBtn:SetScript("OnEnter", function(self)
            if not wcTip then
                wcTip = CreateFrame("GameTooltip","WCTip",UIParent,"GameTooltipTemplate")
                wcTip:SetFrameStrata("TOOLTIP")
            end
            wcTip:SetOwner(self, "ANCHOR_BOTTOM"); wcTip:ClearLines()
            wcTip:AddLine(capturedT.name or "?", r, g, b)
            wcTip:AddLine(" ")
            wcTip:AddLine(FormatGold(capturedT.gold))
            if capturedT.mpRating and capturedT.mpRating > 0 then
                wcTip:AddLine(" ")
                wcTip:AddDoubleLine("史诗钥石评分", tostring(math.floor(capturedT.mpRating)), 1, 0.9, 0.4, 0.2, 0.85, 1)
                if capturedT.mpBestPerMap then
                    local sorted = {}
                    for _, d in ipairs(MYTHIC_DUNGEONS) do
                        local lv = capturedT.mpBestPerMap[d.label] or capturedT.mpBestPerMap[d.koName] or capturedT.mpBestPerMap[d.enName]
                        if lv then
                            table.insert(sorted, { label = d.label, level = lv })
                        end
                    end
                    table.sort(sorted, function(a, b) return a.level > b.level end)
                    for _, entry in ipairs(sorted) do
                        wcTip:AddDoubleLine(entry.label, "+" .. entry.level, 0.82, 0.82, 0.82, 0.4, 1, 0.4)
                    end
                end
            end
            if capturedT.lastSeen and capturedT.dbKey ~= GetToonKey() then
                wcTip:AddLine(" ")
                wcTip:AddLine(WCLL.lastSeen .. date("%Y-%m-%d %H:%M", capturedT.lastSeen), 0.55, 0.55, 0.55)
            end
            wcTip:Show()
        end)
        nameBtn:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
        nameBtn:SetScript("OnClick", function(self, btn)
            if btn == "RightButton" then
                if wcTip then wcTip:Hide() end
                if wcToonMenu and wcToonMenu:IsShown() then
                    wcToonMenu:Hide()
                    return
                end
                if not wcToonMenu then
                    wcToonMenu = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
                    wcToonMenu:SetSize(166, 34)
                    wcToonMenu:SetFrameStrata("TOOLTIP")
                    wcToonMenu:SetBackdrop({ bgFile="Interface\\Tooltips\\UI-Tooltip-Background",
                        edgeFile="Interface\\Tooltips\\UI-Tooltip-Border", edgeSize=10, insets={left=3,right=3,top=3,bottom=3} })
                    wcToonMenu:SetBackdropColor(0.04,0.04,0.05,0.95)
                    wcToonMenu:SetBackdropBorderColor(0.18,0.18,0.20,1)
                    local tmbtn = CreateFrame("Button", nil, wcToonMenu)
                    tmbtn:SetSize(148, 22); tmbtn:SetPoint("TOP", wcToonMenu, "TOP", 0, -6)
                    local tmhl = tmbtn:CreateTexture(nil,"HIGHLIGHT")
                    tmhl:SetColorTexture(1,1,1,0.10); tmhl:SetAllPoints()
                    local tmlbl = tmbtn:CreateFontString(nil,"OVERLAY","GameFontNormal")
                    local f2,_,fl2 = GameFontNormal:GetFont()
                    tmlbl:SetFont(f2,11,fl2 or ""); tmlbl:SetTextColor(1.00,0.45,0.45)
                    tmlbl:SetAllPoints(); tmlbl:SetJustifyH("CENTER")
                    wcToonMenu.lbl = tmlbl
                    local tmClose = CreateFrame("Frame", nil, UIParent)
                    tmClose:SetAllPoints(); tmClose:SetFrameStrata("HIGH")
                    tmClose:EnableMouse(true); tmClose:Hide()
                    tmClose:SetScript("OnMouseDown", function() wcToonMenu:Hide(); tmClose:Hide() end)
                    wcToonMenu:SetScript("OnShow", function() tmClose:Show() end)
                    wcToonMenu:SetScript("OnHide", function() tmClose:Hide() end)
                    tmbtn:SetScript("OnClick", function()
                        wcToonMenu:Hide()
                        if wcToonMenuKey then
                            WeeklyChecklistDB.hiddenToons = WeeklyChecklistDB.hiddenToons or {}
                            WeeklyChecklistDB.hiddenToons[wcToonMenuKey] = true
                            wcToonMenuKey = nil
                            BuildUI()
                        end
                    end)
                end
                if wcToonMenu.lbl then wcToonMenu.lbl:SetText(WCLL.menuHideToon) end
                wcToonMenuKey = capturedT.dbKey
                wcToonMenu:ClearAllPoints()
                wcToonMenu:SetPoint("TOP", self, "BOTTOM", 0, -2)
                wcToonMenu:Show()
            end
        end)
    end
    local hl = hdr:CreateTexture(nil,"ARTWORK")
    hl:SetColorTexture(unpack(C.line)); hl:SetHeight(1)
    hl:SetPoint("BOTTOMLEFT", hdr, "BOTTOMLEFT", 0, 0); hl:SetWidth(totalW-PAD_L*2)
    local curY = bY - ROW_H
    local visRow = 0
    local curSectionKey = nil
    for _, row in ipairs(ROWS) do
        if row.type == "sep" then
            curSectionKey = row.label
            local isCollapsed = collapsed[curSectionKey]
            local sf = CreateFrame("Button", nil, mainFrame)
            sf.wcManaged = true
            sf:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", bX, curY)
            sf:SetSize(totalW-PAD_L*2, ROW_H)
            local sbg = sf:CreateTexture(nil,"BACKGROUND")
            sbg:SetColorTexture(unpack(C.sepBg)); sbg:SetAllPoints()
            local accent = sf:CreateTexture(nil,"ARTWORK")
            accent:SetColorTexture(0.30,0.55,0.90,1); accent:SetWidth(2)
            accent:SetPoint("LEFT",sf,"LEFT",0,0); accent:SetHeight(ROW_H)
            local lr,lg,lb = unpack(C.txtHeader)
            local arrow = isCollapsed and "▶ " or "▼ "
            local sepText = arrow..row.label
            if row.label == WCLL.sepMythicPlus then
                local lbl2 = Lbl(sf, sepText, lr,lg,lb, "LEFT", FONT_XS)
                lbl2:SetPoint("TOPLEFT",sf,"TOPLEFT",8,0); lbl2:SetSize(totalW-PAD_L*2-8, ROW_H)
            else
                local lbl = Lbl(sf, sepText, lr,lg,lb, "LEFT", FONT_XS)
                lbl:SetPoint("TOPLEFT",sf,"TOPLEFT",8,0); lbl:SetSize(totalW-PAD_L*2-8, ROW_H)
            end
            local hlTex = sf:CreateTexture(nil,"HIGHLIGHT")
            hlTex:SetColorTexture(1,1,1,0.07); hlTex:SetAllPoints()
            local capturedKey = curSectionKey
            sf:SetScript("OnClick", function()
                collapsed[capturedKey] = not collapsed[capturedKey]
                BuildUI()
            end)
            curY = curY - ROW_H
        elseif curSectionKey and collapsed[curSectionKey] then
        elseif row.rowKey and hiddenCurrencyRows[row.rowKey] then

        else
            visRow = visRow + 1
            local thisH = (row.type == "mythicPlus") and math.floor(ROW_H * 1.55) or ROW_H
            local rf = CreateFrame("Frame", nil, mainFrame)
            rf.wcManaged = true
            rf:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", bX, curY)
            rf:SetSize(totalW-PAD_L*2, thisH)
            local rbg = rf:CreateTexture(nil,"BACKGROUND")
            if visRow%2==0 then rbg:SetColorTexture(unpack(C.rowEven))
            else                 rbg:SetColorTexture(unpack(C.rowOdd)) end
            rbg:SetAllPoints()
            local lr,lg,lb = unpack(C.txtNormal)
            if row.type=="dungeon" then lr,lg,lb = unpack(C.txtDung) end
            if row.type=="raid" then lr,lg,lb = unpack(C.txtRaid) end
            if row.type=="raidMerged" then lr,lg,lb = unpack(C.txtRaidLabel) end
            if row.type=="info" or row.type=="flame" or row.type=="crest" or row.type=="prey"
            or row.type=="manaVessel" or row.type=="voidCore" or row.type=="vaultKey" or row.type=="keyFragment"
            or row.type=="dundunShard" or row.type=="worldBoss" or row.type=="delve"
            or row.type=="mythicPlus" or row.type=="treasureGrace" or row.type=="keystone" or row.type=="ascendShard" then
                lr,lg,lb = unpack(row.cr)
            end
            local rowLabel = row.label
            if row.type=="worldBoss" then
                local firstName = nil
                for _, t2 in ipairs(toons) do
                    if t2.worldBossName then
                        local nd = t2.worldBossName
                        if type(nd) == "table" then
                            firstName = useEnglish and nd.en or nd.ko
                        else
                            firstName = nd
                        end
                        break
                    end
                end
                if firstName then
                    rowLabel = firstName .. " (" .. WCLL.worldBoss .. ")"
                else
                    rowLabel = WCLL.worldBoss
                end
            end
            local labelX = 6
            local rowTypeHasIcon = { flame=true, crest=true, manaVessel=true, voidCore=true, vaultKey=true, keyFragment=true, dundunShard=true, treasureGrace=true, ascendShard=true }
            if rowTypeHasIcon[row.type] then
                local iSz = math.max(8, ROW_H - 8)
                local iconTex = rf:CreateTexture(nil, "ARTWORK")
                iconTex:SetSize(iSz, iSz)
                iconTex:SetPoint("LEFT", rf, "LEFT", labelX, 0)
                local fid
                if row.type == "flame" then
                    fid = 7551418
                elseif row.type == "crest" then
                    local ci = C_CurrencyInfo.GetCurrencyInfo(row.crestID)
                    fid = ci and ci.iconFileID
                elseif row.type == "manaVessel" then
                    local ci = C_CurrencyInfo.GetCurrencyInfo(MANA_VESSEL_CURRENCY_ID)
                    fid = ci and ci.iconFileID
                elseif row.type == "voidCore" then
                    local ci = C_CurrencyInfo.GetCurrencyInfo(VOID_CORE_CURRENCY_ID)
                    fid = ci and ci.iconFileID
                elseif row.type == "ascendShard" then
                    fid = 7658133
                elseif row.type == "vaultKey" then
                    local ci = C_CurrencyInfo.GetCurrencyInfo(VAULT_KEY_CURRENCY_ID)
                    fid = ci and ci.iconFileID
                elseif row.type == "keyFragment" then
                    local ci = C_CurrencyInfo.GetCurrencyInfo(KEY_FRAGMENT_CURRENCY_ID)
                    fid = ci and ci.iconFileID
                elseif row.type == "dundunShard" then
                    local ci = C_CurrencyInfo.GetCurrencyInfo(DUNDUN_SHARD_CURRENCY_ID)
                    fid = ci and ci.iconFileID
                elseif row.type == "treasureGrace" then
                    fid = 1064187
                end
                if fid then
                    iconTex:SetTexture(fid)
                    iconTex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
                else
                    iconTex:Hide()
                end
                labelX = labelX + iSz + 2
            end
            local lbl = Lbl(rf, rowLabel, lr,lg,lb, "LEFT", FONT_XS)
            lbl:SetPoint("TOPLEFT",rf,"TOPLEFT",labelX,0); lbl:SetSize(NAME_W-8-(labelX-6), thisH)

            if row.rowKey then
                local lblBtn = CreateFrame("Button", nil, rf)
                lblBtn:SetPoint("TOPLEFT", rf, "TOPLEFT", 0, 0)
                lblBtn:SetSize(NAME_W, thisH)
                lblBtn:RegisterForClicks("RightButtonUp")
                local capturedRowKey = row.rowKey
                local capturedRowLabel = rowLabel
                lblBtn:SetScript("OnClick", function(self, btn)
                    if btn == "RightButton" then
                        if not wcCurrRowMenu then
                            wcCurrRowMenu = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
                            wcCurrRowMenu:SetSize(180, 34)
                            wcCurrRowMenu:SetFrameStrata("TOOLTIP")
                            wcCurrRowMenu:SetBackdrop({ bgFile="Interface\\Tooltips\\UI-Tooltip-Background",
                                edgeFile="Interface\\Tooltips\\UI-Tooltip-Border", edgeSize=10, insets={left=3,right=3,top=3,bottom=3} })
                            wcCurrRowMenu:SetBackdropColor(0.04,0.04,0.05,0.95)
                            wcCurrRowMenu:SetBackdropBorderColor(0.18,0.18,0.20,1)
                            local cmbtn = CreateFrame("Button", nil, wcCurrRowMenu)
                            cmbtn:SetSize(162, 22); cmbtn:SetPoint("TOP", wcCurrRowMenu, "TOP", 0, -6)
                            local cmhl = cmbtn:CreateTexture(nil,"HIGHLIGHT")
                            cmhl:SetColorTexture(1,1,1,0.10); cmhl:SetAllPoints()
                            local cmlbl = cmbtn:CreateFontString(nil,"OVERLAY","GameFontNormal")
                            local f3,_,fl3 = GameFontNormal:GetFont()
                            cmlbl:SetFont(f3,11,fl3 or ""); cmlbl:SetTextColor(1.00,0.65,0.20)
                            cmlbl:SetAllPoints(); cmlbl:SetJustifyH("CENTER")
                            wcCurrRowMenu.lbl = cmlbl
                            local cmClose = CreateFrame("Frame", nil, UIParent)
                            cmClose:SetAllPoints(); cmClose:SetFrameStrata("HIGH")
                            cmClose:EnableMouse(true); cmClose:Hide()
                            cmClose:SetScript("OnMouseDown", function() wcCurrRowMenu:Hide(); cmClose:Hide() end)
                            wcCurrRowMenu:SetScript("OnShow", function() cmClose:Show() end)
                            wcCurrRowMenu:SetScript("OnHide", function() cmClose:Hide() end)
                            cmbtn:SetScript("OnClick", function()
                                wcCurrRowMenu:Hide()
                                if wcCurrRowMenuKey then
                                    WeeklyChecklistDB.hiddenCurrencyRows = WeeklyChecklistDB.hiddenCurrencyRows or {}
                                    WeeklyChecklistDB.hiddenCurrencyRows[wcCurrRowMenuKey] = wcCurrRowMenuLabel
                                    wcCurrRowMenuKey = nil
                                    wcCurrRowMenuLabel = nil
                                    BuildUI()
                                end
                            end)
                            wcCurrRowMenu.btn = cmbtn
                        end
                        if wcCurrRowMenu.lbl then wcCurrRowMenu.lbl:SetText(WCLL.menuHideCurrency) end
                        wcCurrRowMenuKey = capturedRowKey
                        wcCurrRowMenuLabel = capturedRowLabel
                        wcCurrRowMenu:ClearAllPoints()
                        wcCurrRowMenu:SetPoint("TOP", self, "BOTTOM", 0, -2)
                        wcCurrRowMenu:Show()
                    end
                end)
            end
            for col, t in ipairs(toons) do
                local xOff = NAME_W+(col-1)*TOON_W
                local vl = rf:CreateTexture(nil,"ARTWORK")
                vl:SetColorTexture(unpack(C.line)); vl:SetWidth(1)
                vl:SetPoint("TOPLEFT",rf,"TOPLEFT",xOff-1,0); vl:SetHeight(thisH)
                if row.type=="info" then
                    local val = t[row.key] or 0
                    local r2,g2,b2 = unpack(row.cr)
                    local vf = Lbl(rf, string.format("%.1f", val), r2,g2,b2, "CENTER", FONT_S)
                    vf:SetPoint("TOPLEFT",rf,"TOPLEFT",xOff,0); vf:SetSize(TOON_W,ROW_H)
                elseif row.type=="flame" then
                    local val = t.radiantFlame or 0
                    local r2,g2,b2 = unpack(row.cr)
                    local cap = GetRadiantFlameCap()
                    local cell = CreateFrame("Frame", nil, rf)
                    cell:SetPoint("TOPLEFT",rf,"TOPLEFT",xOff,0); cell:SetSize(TOON_W,ROW_H)
                    local vf = cell:CreateFontString(nil,"OVERLAY"); vf:SetFont(FF(FONT_S))
                    vf:SetText(tostring(val).."/"..tostring(cap)); vf:SetTextColor(r2,g2,b2)
                    vf:SetJustifyH("CENTER"); vf:SetAllPoints()
                    if col == 1 and t.radiantFlame ~= nil then
                        cell:EnableMouse(true)
                        cell:SetScript("OnEnter", function(self)
                            if not wcTip then
                                wcTip = CreateFrame("GameTooltip","WCTip",UIParent,"GameTooltipTemplate")
                                wcTip:SetFrameStrata("TOOLTIP")
                            end
                            wcTip:SetOwner(self, "ANCHOR_RIGHT")
                            wcTip:SetCurrencyByID(3212)
                            wcTip:Show()
                        end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    elseif col > 1 and t.currencyTooltips and t.currencyTooltips[3212] then
                        cell:EnableMouse(true)
                        local saved = t.currencyTooltips[3212]
                        cell:SetScript("OnEnter", function(self) ShowSavedCurrencyTip(self, saved) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    end
                elseif row.type=="crest" then
                    local qty = (t.dawncrest and t.dawncrest[row.crestID]) or 0
                    local r2,g2,b2 = unpack(row.cr)
                    local cell = CreateFrame("Frame", nil, rf)
                    cell:SetPoint("TOPLEFT",rf,"TOPLEFT",xOff,0); cell:SetSize(TOON_W,ROW_H)
                    local vf = cell:CreateFontString(nil,"OVERLAY"); vf:SetFont(FF(FONT_S))
                    vf:SetText(tostring(qty)); vf:SetTextColor(r2,g2,b2)
                    vf:SetJustifyH("CENTER"); vf:SetAllPoints()
                    if col == 1 then
                        cell:EnableMouse(true)
                        local cid = row.crestID
                        cell:SetScript("OnEnter", function(self) ShowCurrencyTip(self, cid) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    elseif t.currencyTooltips and t.currencyTooltips[row.crestID] then
                        cell:EnableMouse(true)
                        local saved = t.currencyTooltips[row.crestID]
                        cell:SetScript("OnEnter", function(self) ShowSavedCurrencyTip(self, saved) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    end
                elseif row.type=="vaultKey" then
                    local qty = t.vaultKey or 0
                    local r2,g2,b2 = unpack(row.cr)
                    local cell = CreateFrame("Frame", nil, rf)
                    cell:SetPoint("TOPLEFT",rf,"TOPLEFT",xOff,0); cell:SetSize(TOON_W,ROW_H)
                    local vf = cell:CreateFontString(nil,"OVERLAY"); vf:SetFont(FF(FONT_S))
                    vf:SetText(tostring(qty)); vf:SetTextColor(r2,g2,b2)
                    vf:SetJustifyH("CENTER"); vf:SetAllPoints()
                    if col == 1 then
                        cell:EnableMouse(true)
                        cell:SetScript("OnEnter", function(self) ShowCurrencyTip(self, VAULT_KEY_CURRENCY_ID) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    elseif t.currencyTooltips and t.currencyTooltips[VAULT_KEY_CURRENCY_ID] then
                        cell:EnableMouse(true)
                        local saved = t.currencyTooltips[VAULT_KEY_CURRENCY_ID]
                        cell:SetScript("OnEnter", function(self) ShowSavedCurrencyTip(self, saved) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    end
                elseif row.type=="keyFragment" then
                    local frag = t.keyFragment or { current=0, weekly=0 }
                    local r2,g2,b2 = unpack(row.cr)
                    local txt = tostring(frag.current)
                    local cell = CreateFrame("Frame", nil, rf)
                    cell:SetPoint("TOPLEFT",rf,"TOPLEFT",xOff,0); cell:SetSize(TOON_W,ROW_H)
                    local vf = cell:CreateFontString(nil,"OVERLAY"); vf:SetFont(FF(FONT_S))
                    vf:SetText(txt); vf:SetTextColor(r2,g2,b2)
                    vf:SetJustifyH("CENTER"); vf:SetAllPoints()
                    if col == 1 then
                        cell:EnableMouse(true)
                        cell:SetScript("OnEnter", function(self) ShowCurrencyTip(self, KEY_FRAGMENT_CURRENCY_ID) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    elseif t.currencyTooltips and t.currencyTooltips[KEY_FRAGMENT_CURRENCY_ID] then
                        cell:EnableMouse(true)
                        local saved = t.currencyTooltips[KEY_FRAGMENT_CURRENCY_ID]
                        cell:SetScript("OnEnter", function(self) ShowSavedCurrencyTip(self, saved) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    end
                elseif row.type=="manaVessel" then
                    local mv = t.manaVessel or { current=0, totalEarned=0 }
                    local r2,g2,b2 = unpack(row.cr)
                    local cell = CreateFrame("Frame", nil, rf)
                    cell:SetPoint("TOPLEFT",rf,"TOPLEFT",xOff,0); cell:SetSize(TOON_W,ROW_H)
                    local vf = cell:CreateFontString(nil,"OVERLAY"); vf:SetFont(FF(FONT_S))
                    vf:SetText(tostring(mv.current)); vf:SetTextColor(r2,g2,b2)
                    vf:SetJustifyH("CENTER"); vf:SetAllPoints()
                    if col == 1 then
                        cell:EnableMouse(true)
                        cell:SetScript("OnEnter", function(self) ShowCurrencyTip(self, MANA_VESSEL_CURRENCY_ID) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    elseif t.currencyTooltips and t.currencyTooltips[MANA_VESSEL_CURRENCY_ID] then
                        cell:EnableMouse(true)
                        local saved = t.currencyTooltips[MANA_VESSEL_CURRENCY_ID]
                        cell:SetScript("OnEnter", function(self) ShowSavedCurrencyTip(self, saved) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    end
                elseif row.type=="voidCore" then
                    local vc = t.voidCore or { current=0, totalEarned=0 }
                    local r2,g2,b2 = unpack(row.cr)
                    local cell = CreateFrame("Frame", nil, rf)
                    cell:SetPoint("TOPLEFT",rf,"TOPLEFT",xOff,0); cell:SetSize(TOON_W,ROW_H)
                    local vf = cell:CreateFontString(nil,"OVERLAY"); vf:SetFont(FF(FONT_S))
                    vf:SetText(tostring(vc.current)); vf:SetTextColor(r2,g2,b2)
                    vf:SetJustifyH("CENTER"); vf:SetAllPoints()
                    if col == 1 then
                        cell:EnableMouse(true)
                        cell:SetScript("OnEnter", function(self) ShowCurrencyTip(self, VOID_CORE_CURRENCY_ID) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    elseif t.currencyTooltips and t.currencyTooltips[VOID_CORE_CURRENCY_ID] then
                        cell:EnableMouse(true)
                        local saved = t.currencyTooltips[VOID_CORE_CURRENCY_ID]
                        cell:SetScript("OnEnter", function(self) ShowSavedCurrencyTip(self, saved) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    end
                elseif row.type=="ascendShard" then
                    local qty = t.ascendShard or 0
                    local r2,g2,b2 = unpack(row.cr)
                    local cell = CreateFrame("Frame", nil, rf)
                    cell:SetPoint("TOPLEFT",rf,"TOPLEFT",xOff,0); cell:SetSize(TOON_W,ROW_H)
                    local vf = cell:CreateFontString(nil,"OVERLAY"); vf:SetFont(FF(FONT_S))
                    vf:SetText(tostring(qty)); vf:SetTextColor(r2,g2,b2)
                    vf:SetJustifyH("CENTER"); vf:SetAllPoints()
                    if col == 1 then
                        cell:EnableMouse(true)
                        cell:SetScript("OnEnter", function(self)
                            if not wcTip then
                                wcTip = CreateFrame("GameTooltip","WCTip",UIParent,"GameTooltipTemplate")
                                wcTip:SetFrameStrata("TOOLTIP")
                            end
                            wcTip:SetOwner(self, "ANCHOR_RIGHT")
                            wcTip:SetHyperlink("item:"..ASCEND_SHARD_ITEM_ID)
                            wcTip:Show()
                        end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    elseif t.itemTooltips and t.itemTooltips[ASCEND_SHARD_ITEM_ID] then
                        cell:EnableMouse(true)
                        local saved = t.itemTooltips[ASCEND_SHARD_ITEM_ID]
                        cell:SetScript("OnEnter", function(self) ShowSavedCurrencyTip(self, saved) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    end
                elseif row.type=="dundunShard" then
                    local ds = t.dundunShard or { current=0, weeklyEarned=0, maxWeekly=8, maxTotal=8 }
                    local r2,g2,b2 = unpack(row.cr)
                    local txt = tostring(ds.current)
                    local cell = CreateFrame("Frame", nil, rf)
                    cell:SetPoint("TOPLEFT",rf,"TOPLEFT",xOff,0); cell:SetSize(TOON_W,ROW_H)
                    local vf = cell:CreateFontString(nil,"OVERLAY"); vf:SetFont(FF(FONT_S))
                    vf:SetText(txt); vf:SetTextColor(r2,g2,b2)
                    vf:SetJustifyH("CENTER"); vf:SetAllPoints()
                    if col == 1 then
                        cell:EnableMouse(true)
                        cell:SetScript("OnEnter", function(self) ShowCurrencyTip(self, DUNDUN_SHARD_CURRENCY_ID) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    elseif t.currencyTooltips and t.currencyTooltips[DUNDUN_SHARD_CURRENCY_ID] then
                        cell:EnableMouse(true)
                        local saved = t.currencyTooltips[DUNDUN_SHARD_CURRENCY_ID]
                        cell:SetScript("OnEnter", function(self) ShowSavedCurrencyTip(self, saved) end)
                        cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                    end
                elseif row.type=="dungeon" then
                    local d = t.dungeons and t.dungeons[row.dungName]
                    local k = d and d.killed or 0
                    local n = d and d.total  or row.def
                    ProgBtn(rf, xOff, k, n, d and d.bosses or {}, row.label, row.koName)
                elseif row.type=="raid" then
                    local rd = t.raids and t.raids[row.raidName]
                    local dd = rd and rd[row.diffID]
                    local k  = dd and dd.killed or 0
                    local n  = dd and dd.total  or row.def
                    ProgBtn(rf, xOff, k, n, dd and dd.bosses or {}, row.label, nil)
                elseif row.type=="raidMerged" then
                    local rd = t.raids and t.raids[row.raidName]
                    local bestDiffID = GetHighestRaidDiff(rd)
                    local k, n, dispText
                    if bestDiffID then
                        local dd = rd[bestDiffID]
                        k = dd.killed or 0
                        n = dd.total  or row.def
                        local dLabel = WCLL.diffLabel[bestDiffID] or tostring(bestDiffID)
                        dispText = dLabel.." "..k.."/"..n
                    else
                        k = 0
                        local fallback = rd and rd[14]
                        n = (fallback and fallback.total) or row.def
                        dispText = k.."/"..n
                    end
                    local r2,g2,b2 = ProgColor(k, n)
                    local btn = CreateFrame("Frame", nil, rf)
                    btn:SetPoint("TOPLEFT", rf, "TOPLEFT", xOff, 0)
                    btn:SetSize(TOON_W, ROW_H); btn:EnableMouse(true)
                    local fs = btn:CreateFontString(nil,"OVERLAY"); fs:SetFont(FF(FONT_S))
                    fs:SetText(dispText); fs:SetTextColor(r2,g2,b2)
                    fs:SetJustifyH("CENTER"); fs:SetAllPoints()
                    local capturedRd = rd
                    local capturedLabel = row.label
                    btn:SetScript("OnEnter", function(self) ShowRaidMergedTip(self, capturedLabel, capturedRd) end)
                    btn:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                elseif row.type=="prey" then
                    local pd = t.prey and t.prey[row.preyKey]
                    local k  = pd and pd.killed or 0
                    local n  = pd and pd.total  or row.def
                    ProgBtn(rf, xOff, k, n, {}, row.label, nil)
                elseif row.type=="worldBoss" then
                    local killed = t.worldBossKilled or 0
                    local r2,g2,b2 = ProgColor(killed, 1)
                    local vf = Lbl(rf, tostring(killed).."/1", r2,g2,b2, "CENTER", FONT_S)
                    vf:SetPoint("TOPLEFT",rf,"TOPLEFT",xOff,0); vf:SetSize(TOON_W,ROW_H)
                elseif row.type=="delve" then
                    local dv = t.delves or { total=0, tiers={} }
                    local total = dv.total or 0
                    local r2,g2,b2
                    if total > 0 then r2,g2,b2 = unpack(C.kill) else r2,g2,b2 = unpack(C.zero) end
                    local cell = CreateFrame("Frame", nil, rf)
                    cell:SetPoint("TOPLEFT", rf, "TOPLEFT", xOff, 0)
                    cell:SetSize(TOON_W, ROW_H); cell:EnableMouse(true)
                    local vf = cell:CreateFontString(nil,"OVERLAY"); vf:SetFont(FF(FONT_S))
                    vf:SetText(tostring(total)); vf:SetTextColor(r2,g2,b2)
                    vf:SetJustifyH("CENTER"); vf:SetAllPoints()
                    local capturedDv = dv
                    local capturedName = t.name
                    cell:SetScript("OnEnter", function(self) ShowDelveTip(self, capturedName, capturedDv) end)
                    cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                elseif row.type=="mythicPlus" then                    local mp = t.mythicPlus or { runs={} }
                    local runs = mp.runs or {}
                    local sorted = {}
                    for _, r in ipairs(runs) do table.insert(sorted, r) end
                    table.sort(sorted, function(a, b) return (a.level or 0) > (b.level or 0) end)
                    local total = #sorted
                    local slot1 = sorted[1]
                    local slot4 = sorted[4]
                    local slot8 = sorted[8]
                    local ilvl1 = slot1 and GetMythicPlusVaultIlvl(slot1.level)
                    local ilvl4 = slot4 and GetMythicPlusVaultIlvl(slot4.level)
                    local ilvl8 = slot8 and GetMythicPlusVaultIlvl(slot8.level)
                    local function slotColor(have, need)
                        if have >= need then return unpack(C.kill)
                        else return unpack(C.zero) end
                    end
                    local r1,g1,b1 = slotColor(total,1)
                    local r4,g4,b4 = slotColor(total,4)
                    local r8,g8,b8 = slotColor(total,8)
                    local cell = CreateFrame("Frame", nil, rf)
                    cell:SetPoint("TOPLEFT", rf, "TOPLEFT", xOff, 0)
                    cell:SetSize(TOON_W, thisH); cell:EnableMouse(true)
                    local hitTex = cell:CreateTexture(nil, "BACKGROUND")
                    hitTex:SetColorTexture(0, 0, 0, 0); hitTex:SetAllPoints()
                    local divW = 6
                    local colW = math.floor((TOON_W - divW * 2) / 3)
                    local col1X = 0
                    local div1X = col1X + colW
                    local col4X = div1X + divW
                    local div2X = col4X + colW
                    local col8X = div2X + divW
                    local halfH = math.floor(thisH / 2)
                    local lineGap = math.floor(thisH * 0.22)
                    local function MakeValCol(px, countNum, ilvlStr, cr, cg, cb)
                        local top = cell:CreateFontString(nil,"OVERLAY"); top:SetFont(FF(FONT_XS))
                        top:SetText(tostring(countNum))
                        top:SetTextColor(cr,cg,cb)
                        top:SetJustifyH("CENTER")
                        top:SetPoint("TOPLEFT", cell, "TOPLEFT", px, 0)
                        top:SetSize(colW, halfH)
                        local bot = cell:CreateFontString(nil,"OVERLAY"); bot:SetFont(FF(FONT_XS))
                        bot:SetText(ilvlStr)
                        bot:SetTextColor(cr,cg,cb)
                        bot:SetJustifyH("CENTER")
                        bot:SetPoint("TOPLEFT", cell, "TOPLEFT", px, -halfH)
                        bot:SetSize(colW, halfH)
                    end
                    local function MakeDivLine(px)
                        local ln = cell:CreateTexture(nil,"ARTWORK")
                        ln:SetColorTexture(0.16, 0.16, 0.16, 1)
                        ln:SetWidth(1)
                        ln:SetPoint("TOPLEFT",    cell, "TOPLEFT",    px + divW/2, -lineGap)
                        ln:SetPoint("BOTTOMLEFT", cell, "BOTTOMLEFT", px + divW/2,  lineGap)
                    end
                    local i1str = ilvl1 and tostring(ilvl1) or "-"
                    local i4str = ilvl4 and tostring(ilvl4) or "-"
                    local i8str = ilvl8 and tostring(ilvl8) or "-"
                    MakeValCol(col1X, 1, i1str, r1,g1,b1)
                    MakeDivLine(div1X)
                    MakeValCol(col4X, 4, i4str, r4,g4,b4)
                    MakeDivLine(div2X)
                    MakeValCol(col8X, 8, i8str, r8,g8,b8)
                    local capturedMp = mp
                    local capturedName = t.name
                    cell:SetScript("OnEnter", function(self) ShowMythicPlusTip(self, capturedName, capturedMp) end)
                    cell:SetScript("OnLeave", function() if wcTip then wcTip:Hide() end end)
                elseif row.type=="keystone" then
                    local ks = t.keystone
                    local r2, g2, b2 = unpack(row.cr)
                    local cell = CreateFrame("Frame", nil, rf)
                    cell:SetPoint("TOPLEFT", rf, "TOPLEFT", xOff, 0); cell:SetSize(TOON_W, ROW_H)
                    if ks and ks.level then
                        local shortName
                        if useEnglish then
                            shortName = KEYSTONE_SHORT_EN[ks.mapName] or ks.mapName or ""
                        else
                            shortName = KEYSTONE_SHORT_KO[ks.mapName] or ks.mapName or ""
                        end
                        local lvlStr = Lbl(cell, "+"..ks.level, 1.00, 0.82, 0.00, "RIGHT", FONT_XS)
                        lvlStr:SetPoint("TOPLEFT", cell, "TOPLEFT", 0, 0)
                        lvlStr:SetSize(math.floor(TOON_W * 0.35), ROW_H)
                        local nameStr = Lbl(cell, shortName, 0.72, 0.72, 0.72, "LEFT", FONT_S)
                        nameStr:SetPoint("TOPLEFT", cell, "TOPLEFT", math.floor(TOON_W * 0.38), 0)
                        nameStr:SetSize(math.floor(TOON_W * 0.62), ROW_H)
                    else
                        local vf = Lbl(cell, WCLL.keystoneNone, unpack(C.zero))
                        vf:SetJustifyH("CENTER"); vf:SetAllPoints()
                    end
                elseif row.type=="treasureGrace" then
                    local done = (t.treasureHunterGrace or 0) == 1
                    local r2, g2, b2
                    if done then r2,g2,b2 = unpack(C.kill) else r2,g2,b2 = unpack(C.zero) end
                    local txt = done and WCLL.graceAcquired or WCLL.graceNotYet
                    local vf = Lbl(rf, txt, r2, g2, b2, "CENTER", FONT_S)
                    vf:SetPoint("TOPLEFT", rf, "TOPLEFT", xOff, 0); vf:SetSize(TOON_W, ROW_H)
                end
            end
            local rl = rf:CreateTexture(nil,"ARTWORK")
            rl:SetColorTexture(0.14,0.14,0.16,0.7); rl:SetHeight(1)
            rl:SetPoint("BOTTOMLEFT",rf,"BOTTOMLEFT",0,0); rl:SetWidth(totalW-PAD_L*2)
            curY = curY - thisH
        end
    end
    local myData = WeeklyChecklistDB[GetToonKey()]
    local updStr = ""
    if myData and myData.lastUpdate then
        updStr = date(WCLL.updPrefix.."%Y-%m-%d %H:%M", myData.lastUpdate)
    end
    local foot = mainFrame:CreateFontString(nil,"OVERLAY")
    foot.wcManaged = true
    foot:SetFont(FF(9)); foot:SetText(updStr)
    foot:SetTextColor(0.45,0.45,0.45); foot:SetJustifyH("RIGHT")
    foot:SetPoint("BOTTOMRIGHT",mainFrame,"BOTTOMRIGHT",-8,6)
    foot:SetSize(totalW-20,16)
    mainFrame:Show()
end
SLASH_WEEKLYCHECKLIST1 = "/wc"
SLASH_WEEKLYCHECKLIST2 = "/weeklychecklist"
SlashCmdList["WEEKLYCHECKLIST"] = function(msg)
    msg = strtrim(msg:lower())
    if msg == "list" then
        print("|cFF00FF00WeeklyChecklist|r " .. WCLL.cmdList)
        print("  |cFFFFFF00/wc|r              — " .. WCLL.cmdOpen)
        print("  |cFFFFFF00/wc icon|r          — " .. WCLL.cmdIcon)
        print("  |cFFFFFF00/wc reset|r         — " .. WCLL.cmdReset)
        print("  |cFFFFFF00/wc refresh|r (or |cFFFFFF00"..WCLL.cmdRefreshAlias.."|r) — "..WCLL.cmdRefresh)
        print("  |cFFFFFF00中键|r              — " .. WCLL.cmdMClick)
        print("  |cFFFFFF00Shift+拖动/滚轮|r   — " .. WCLL.cmdKeyTracker)
        if not mainFrame or not mainFrame:IsShown() then BuildUI() else mainFrame:Hide() end
        return
    end
    --if msg == "icon" then
        --if wcIcon then wcIcon:Show(); iconCfg.visible = true; SaveIconCfg() end
        --print("|cFF00FF00WeeklyChecklist|r: " .. WCLL.msgIconShow)
        --return
    --elseif msg == "reset" then
        --local cx = GetScreenWidth() / 2 - iconCfg.size / 2
        --local cy = GetScreenHeight() / 2 - iconCfg.size / 2
        --iconCfg.x = cx; iconCfg.y = cy; SaveIconCfg()
        --if wcIcon then
            --wcIcon:ClearAllPoints()
            --wcIcon:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", cx, cy)
            --wcIcon:Show()
        --end
        --print("|cFF00FF00WeeklyChecklist|r: " .. WCLL.msgReset)
        --return
    --else
    if msg == "refresh" or msg == "r" then
        CollectData()
        print("|cFF00FF00WeeklyChecklist|r: " .. WCLL.msgRefresh)
        return
    end
    if not mainFrame or not mainFrame:IsShown() then BuildUI() else mainFrame:Hide() end
end
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
eventFrame:RegisterEvent("CURRENCY_DISPLAY_UPDATE")
eventFrame:RegisterEvent("BAG_UPDATE_DELAYED")
eventFrame:RegisterEvent("UPDATE_INSTANCE_INFO")
eventFrame:RegisterEvent("QUEST_TURNED_IN")
eventFrame:RegisterEvent("SCENARIO_COMPLETED")
eventFrame:RegisterEvent("CHALLENGE_MODE_COMPLETED")
eventFrame:RegisterEvent("PLAYER_MONEY")
local preyQuestSet = {}
for _, d in ipairs(PREY_DIFFS) do
    for _, qid in ipairs(d.questIDs) do
        preyQuestSet[qid] = true
    end
end
local initialized = false
eventFrame:SetScript("OnEvent", function(self, event, questID)
    if event == "PLAYER_ENTERING_WORLD" then
        if C_MythicPlus and C_MythicPlus.RequestMapInfo then
            C_MythicPlus.RequestMapInfo()
        end
        C_Timer.After(3, function()
            RequestRaidInfo()
            C_Timer.After(1, function()
                ResetExpiredWeeklyData()
                CollectData()
                initialized = true
            end)
        end)
    elseif event == "SCENARIO_COMPLETED" then
        if initialized then
            local scenarioName = C_Scenario.GetInfo()
            if not scenarioName or not scenarioName:find("地下堡") then return end
            local savedSnapshot = delveProgressSnapshot
            local savedCurrentTier = currentDelveTier
            C_Timer.After(1.0, function()
                local detectedTier = DetectCompletedDelveTier()
                if not detectedTier then
                    detectedTier = savedCurrentTier
                end
                if not detectedTier then return end
                local key = GetToonKey()
                WeeklyChecklistDB[key] = WeeklyChecklistDB[key] or {}
                local t = WeeklyChecklistDB[key]
                if not t.delves then
                    t.delves = { total = 0, tiers = {} }
                end
                t.delves.total = (t.delves.total or 0) + 1
                t.delves.tiers[detectedTier] = (t.delves.tiers[detectedTier] or 0) + 1
                currentDelveTier = nil
                delveProgressSnapshot = {}
                if mainFrame and mainFrame:IsShown() then BuildUI() end
            end)
        end
    elseif event == "QUEST_TURNED_IN" then
        if initialized and preyQuestSet[questID] then
            CollectData()
            if mainFrame and mainFrame:IsShown() then BuildUI() end
        end
    elseif event == "CHALLENGE_MODE_COMPLETED" then
        if initialized then
            C_Timer.After(1.0, function()
                CollectData()
                if mainFrame and mainFrame:IsShown() then BuildUI() end
            end)
        end
    elseif initialized then
        CollectData()
        if mainFrame and mainFrame:IsShown() then BuildUI() end
    end
end)
local delveTierFrame = CreateFrame("Frame")
delveTierFrame:RegisterEvent("SCENARIO_CRITERIA_UPDATE")
delveTierFrame:RegisterEvent("SCENARIO_UPDATE")
delveTierFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
delveTierFrame:SetScript("OnEvent", function()
    if C_Scenario.IsInScenario() then
        delveProgressSnapshot = GetDelveProgressSnapshot()
        local tier = TryGetDelveTierFromDiff()
        if tier then currentDelveTier = tier end
    end
end)
local MTP_DUNGEONS = {
    { name = "希纳斯", id = 1254563 },
    { name = "魔导师", id = 1254572 },
    { name = "迈萨拉", id = 1254559 },
    { name = "萨隆",   id = 1254555 },
    { name = "执政团", id = 1254551 },
    { name = "学院",   id = 393273 },
    { name = "风行塔", id = 1254400 },
    { name = "通天峰", id = 159898 },
}
local CMID_TO_SPELL = {
    [559] = 1254563,
    [558] = 1254572,
    [560] = 1254559,
    [556] = 1254555,
    [239] = 1254551,
    [402] = 393273,
    [557] = 1254400,
    [161] = 159898,
}

kt_getPortalSpellID = function(cmid)
    if not cmid or cmid == 0 then return nil end
    return CMID_TO_SPELL[cmid]
end

local MTP_COLS  = 4
local MTP_BTN_W = 52
local MTP_BTN_H = 58
local MTP_PAD   = 6
local mtpIconList = {}
local mtpCDList   = {}

local mtpFrame = CreateFrame("Frame", "MythicTPFrame", UIParent, "BackdropTemplate")
mtpFrame:SetSize(MTP_COLS * MTP_BTN_W + (MTP_COLS + 1) * MTP_PAD,
                 2 * MTP_BTN_H + 3 * MTP_PAD + 20)
mtpFrame:SetFrameStrata("MEDIUM")
mtpFrame:SetBackdrop({
    bgFile   = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    edgeSize = 8,
    insets   = { left = 2, right = 2, top = 2, bottom = 2 },
})
mtpFrame:SetBackdropColor(0, 0, 0, 0.92)
mtpFrame:SetBackdropBorderColor(0.22, 0.22, 0.22, 1)
mtpFrame:SetMovable(true)
mtpFrame:SetClampedToScreen(true)
mtpFrame:Hide()

local mtpTitleBar = CreateFrame("Frame", nil, mtpFrame)
mtpTitleBar:SetPoint("TOPLEFT",  mtpFrame, "TOPLEFT",  0, 0)
mtpTitleBar:SetPoint("TOPRIGHT", mtpFrame, "TOPRIGHT", 0, 0)
mtpTitleBar:SetHeight(20)
mtpTitleBar:EnableMouse(true)
mtpTitleBar:RegisterForDrag("LeftButton")
mtpTitleBar:SetScript("OnDragStart", function() mtpFrame:StartMoving() end)
mtpTitleBar:SetScript("OnDragStop", function()
    mtpFrame:StopMovingOrSizing()
    WeeklyChecklistDB = WeeklyChecklistDB or {}
    WeeklyChecklistDB.mtpPos = { x = mtpFrame:GetLeft(), y = mtpFrame:GetBottom() }
end)

local mtpTitleText = mtpTitleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
mtpTitleText:SetPoint("LEFT", mtpTitleBar, "LEFT", MTP_PAD, 0)
mtpTitleText:SetText("钥石传送")
mtpTitleText:SetTextColor(0.78, 0.73, 0.48, 1)

local mtpCloseBtn = CreateFrame("Button", nil, mtpFrame)
mtpCloseBtn:SetSize(16, 16)
mtpCloseBtn:SetPoint("TOPRIGHT", mtpFrame, "TOPRIGHT", -4, -2)
mtpCloseBtn:SetFrameLevel(mtpFrame:GetFrameLevel() + 10)
mtpCloseBtn:SetNormalTexture("Interface/Buttons/UI-StopButton")
mtpCloseBtn:GetNormalTexture():SetVertexColor(0.8, 0.2, 0.2, 1)
mtpCloseBtn:SetScript("OnClick", function() mtpFrame:Hide() end)

for i, d in ipairs(MTP_DUNGEONS) do
    local col = (i - 1) % MTP_COLS
    local row = math.floor((i - 1) / MTP_COLS)

    local btn = CreateFrame("Button", "MythicTPBtn"..i, mtpFrame, "SecureActionButtonTemplate")
    btn:SetSize(MTP_BTN_W, MTP_BTN_H)
    btn:SetPoint("TOPLEFT", mtpFrame, "TOPLEFT",
        MTP_PAD + col * (MTP_BTN_W + MTP_PAD),
        -(20 + MTP_PAD + row * (MTP_BTN_H + MTP_PAD)))

    btn:RegisterForClicks("AnyUp", "AnyDown")
    btn:SetAttribute("type",  "spell")
    btn:SetAttribute("spell", d.id)

    local bg = btn:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(); bg:SetColorTexture(0.05, 0.05, 0.05, 1)

    local borderTex = btn:CreateTexture(nil, "BORDER")
    borderTex:SetAllPoints(); borderTex:SetColorTexture(0.16, 0.16, 0.16, 1)

    local icon = btn:CreateTexture(nil, "ARTWORK")
    icon:SetSize(MTP_BTN_W - 4, MTP_BTN_H - 16)
    icon:SetPoint("TOP", btn, "TOP", 0, -2)
    icon:SetColorTexture(0.1, 0.1, 0.1, 1)

    local cd = CreateFrame("Cooldown", "MythicTPBtnCD"..i, btn, "CooldownFrameTemplate")
    cd:SetAllPoints(icon); cd:SetDrawEdge(true)
    mtpCDList[d.id] = cd

    local label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    label:SetPoint("BOTTOM", btn, "BOTTOM", 0, 2)
    label:SetText(d.name); label:SetTextColor(0.7, 0.7, 0.7, 1)

    btn:HookScript("OnEnter", function(self)
        bg:SetColorTexture(0.1, 0.1, 0.1, 1)
        borderTex:SetColorTexture(0.36, 0.36, 0.36, 1)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetSpellByID(d.id)
        GameTooltip:Show()
    end)
    btn:HookScript("OnLeave", function()
        bg:SetColorTexture(0.05, 0.05, 0.05, 1)
        borderTex:SetColorTexture(0.16, 0.16, 0.16, 1)
        GameTooltip:Hide()
    end)

    mtpIconList[d.id] = icon
end

local function MTP_LoadIcons()
    for _, d in ipairs(MTP_DUNGEONS) do
        local tex = C_Spell.GetSpellTexture(d.id)
        if tex and mtpIconList[d.id] then
            mtpIconList[d.id]:SetTexture(tex)
            mtpIconList[d.id]:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        else
            C_Spell.RequestLoadSpellData(d.id)
        end
    end
end

local function MTP_UpdateCooldowns()
    for _, d in ipairs(MTP_DUNGEONS) do
        local cd = mtpCDList[d.id]
        if cd then
            local info = C_Spell.GetSpellCooldown(d.id)
            if info and info.duration and info.duration > 0 then
                cd:SetCooldown(info.startTime, info.duration)
            else
                cd:Clear()
            end
        end
    end
end

local function MTP_Toggle()
    if InCombatLockdown() then return end
    if mtpFrame:IsShown() then mtpFrame:Hide() else mtpFrame:Show() end
end

local mtpLoader = CreateFrame("Frame")
mtpLoader:RegisterEvent("SPELL_DATA_LOAD_RESULT")
mtpLoader:RegisterEvent("SPELL_UPDATE_COOLDOWN")
mtpLoader:RegisterEvent("PLAYER_ENTERING_WORLD")
mtpLoader:SetScript("OnEvent", function(self, event, arg1)
    if event == "PLAYER_ENTERING_WORLD" then
        WeeklyChecklistDB = WeeklyChecklistDB or {}
        local pos = WeeklyChecklistDB.mtpPos
        mtpFrame:ClearAllPoints()
        if pos and pos.x and pos.y then
            mtpFrame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", pos.x, pos.y)
        else
            mtpFrame:SetPoint("CENTER")
        end
        mtpFrame:Hide()
        MTP_LoadIcons()
        MTP_UpdateCooldowns()
    elseif event == "SPELL_DATA_LOAD_RESULT" then
        MTP_LoadIcons()
    elseif event == "SPELL_UPDATE_COOLDOWN" then
        MTP_UpdateCooldowns()
        if kt_frame and WeeklyChecklistDB and WeeklyChecklistDB.ktShown then kt_refresh() end
    end
end)

--[[wcIcon = CreateFrame("Button","WCIconFrame",UIParent)
wcIcon:SetFrameStrata("MEDIUM"); wcIcon:SetSize(iconCfg.size,iconCfg.size)
wcIcon:SetMovable(true); wcIcon:EnableMouse(true); wcIcon:EnableMouseWheel(true)
wcIcon:SetClampedToScreen(true)
wcIcon:RegisterForClicks("LeftButtonUp","RightButtonUp","MiddleButtonUp"); wcIcon:RegisterForDrag("LeftButton")
wcIcon:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", iconCfg.x, iconCfg.y)
local ANIM_TEX   = "Interface\\AddOns\\WeeklyChecklist\\icon\\WCicon_anim"
local NUM_FRAMES = 8
local FRAME_W    = 1 / NUM_FRAMES
local ANIM_TOTAL  = 8.0
local JUMP_START  = 1.8
local JUMP_END    = 2.5
local JUMP_HEIGHT = 12
local FRAME_TIMES = { 0, 0, 0, 0, 3.50, 3.65, 4.00, 4.15 }
local animClock  = 0
local curFrame   = 0
local animActive = false
local wcAnimFrame = CreateFrame("Frame", nil, wcIcon)
wcAnimFrame:SetAllPoints()
local wcTex = wcAnimFrame:CreateTexture(nil,"ARTWORK")
wcTex:SetTexture(ICON_TEX); wcTex:SetAllPoints()
local function SetSpriteFrame(idx)
    wcTex:SetTexCoord(idx * FRAME_W, (idx+1) * FRAME_W, 0, 1)
end
local function StartAnim()
    animActive = true
    animClock = 0; curFrame = 0
    wcTex:SetTexture(ANIM_TEX)
    SetSpriteFrame(0)
end
local function StopAnim()
    animActive = false
    wcAnimFrame:ClearAllPoints(); wcAnimFrame:SetAllPoints()
    wcTex:SetTexture(ICON_TEX)
    wcTex:SetTexCoord(0, 1, 0, 1)
end
wcIcon:SetScript("OnUpdate", function(self, elapsed)
    if not animActive then return end
    animClock = animClock + elapsed
    if animClock >= ANIM_TOTAL then animClock = animClock - ANIM_TOTAL end
    local nf = 0
    for i = NUM_FRAMES, 1, -1 do
        if animClock >= FRAME_TIMES[i] then nf = i - 1; break end
    end
    if nf ~= curFrame then curFrame = nf; SetSpriteFrame(curFrame) end
    local yOff = 0
    if animClock >= JUMP_START and animClock < JUMP_END then
        yOff = math.sin((animClock - JUMP_START) / (JUMP_END - JUMP_START) * math.pi) * JUMP_HEIGHT
    end
    wcAnimFrame:ClearAllPoints()
    wcAnimFrame:SetPoint("BOTTOMLEFT", wcIcon, "BOTTOMLEFT", 0, yOff)
    wcAnimFrame:SetPoint("TOPRIGHT",   wcIcon, "TOPRIGHT",   0, yOff)
end)
local wcMenu = CreateFrame("Frame","WCIconMenu",UIParent,"BackdropTemplate")
wcMenu:SetSize(134,130); wcMenu:SetFrameStrata("TOOLTIP")
wcMenu:SetBackdrop({ bgFile="Interface\\Tooltips\\UI-Tooltip-Background",
    edgeFile="Interface\\Tooltips\\UI-Tooltip-Border", edgeSize=10, insets={left=3,right=3,top=3,bottom=3} })
wcMenu:SetBackdropColor(0.04,0.04,0.05,0.95)
wcMenu:SetBackdropBorderColor(0.18,0.18,0.20,1)
wcMenu:Hide()
local function PositionMenu()
    wcMenu:ClearAllPoints()
    local btnRight = wcIcon:GetRight(); local btnTop = wcIcon:GetTop()
    local screenW  = GetScreenWidth();  local screenH = GetScreenHeight()
    if btnRight + wcMenu:GetWidth() > screenW then
        if btnTop + wcMenu:GetHeight() > screenH then wcMenu:SetPoint("TOPRIGHT",  wcIcon,"BOTTOMLEFT",  -4,0)
        else                                         wcMenu:SetPoint("BOTTOMRIGHT",wcIcon,"TOPLEFT",    -4,0) end
    else
        if btnTop + wcMenu:GetHeight() > screenH then wcMenu:SetPoint("TOPLEFT",   wcIcon,"BOTTOMRIGHT",  4,0)
        else                                         wcMenu:SetPoint("BOTTOMLEFT", wcIcon,"TOPRIGHT",    4,0) end
    end
end
local function MakeMenuBtn(parent, yOff, getLabel, onClick)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(120,28); btn:SetPoint("TOP",parent,"TOP",0,yOff)
    local hl = btn:CreateTexture(nil,"HIGHLIGHT")
    hl:SetColorTexture(1,1,1,0.10); hl:SetAllPoints()
    local lbl = btn:CreateFontString(nil,"OVERLAY","GameFontNormal")
    local f,_,fl = GameFontNormal:GetFont()
    lbl:SetFont(f,11,fl or ""); lbl:SetTextColor(0.90,0.85,0.70)
    lbl:SetAllPoints(); lbl:SetJustifyH("CENTER")
    btn.Refresh = function() lbl:SetText(getLabel()) end
    btn:Refresh()
    btn:SetScript("OnClick",  function() wcMenu:Hide(); onClick() end)
    btn:SetScript("OnEnter",  function() lbl:SetAlpha(0.75) end)
    btn:SetScript("OnLeave",  function() lbl:SetAlpha(1.00) end)
    return btn
end
local lockBtn = MakeMenuBtn(wcMenu, -8,
    function() return iconCfg.locked and WCLL.menuLocked or WCLL.menuUnlocked end,
    function() iconCfg.locked = not iconCfg.locked; SaveIconCfg() end)
local hideIconBtn = MakeMenuBtn(wcMenu, -38,
    function() return WCLL.menuHideIcon end,
    function() wcIcon:Hide(); iconCfg.visible = false; SaveIconCfg() end)
local animMenuBtn = MakeMenuBtn(wcMenu, -68,
    function()
        if useEnglish then
            return animActive and "|cFF44FF44owo (on)|r" or "owo"
        else
            return animActive and "|cFF44FF44嗷! (开启)|r" or "嗷!"
        end
    end,
    function() if animActive then StopAnim() else StartAnim() end end)
local mmMenuBtn = MakeMenuBtn(wcMenu, -98,
    function()
        local hidden = WeeklyChecklistDB and WeeklyChecklistDB.minimap and WeeklyChecklistDB.minimap.hide
        return (not hidden) and WCLL.menuMmOn or WCLL.menuMmOff
    end,
    function()
        local LibDBIcon = LibStub and LibStub("LibDBIcon-1.0", true)
        if not LibDBIcon or not LibDBIcon:IsRegistered("WeeklyChecklist") then return end
        if WeeklyChecklistDB and WeeklyChecklistDB.minimap and WeeklyChecklistDB.minimap.hide then
            LibDBIcon:Show("WeeklyChecklist")
            iconCfg.mmVisible = true
        else
            LibDBIcon:Hide("WeeklyChecklist")
            iconCfg.mmVisible = false
        end
        SaveIconCfg()
    end)
local menuClose = CreateFrame("Frame", nil, UIParent)
menuClose:SetAllPoints(); menuClose:SetFrameStrata("HIGH")
menuClose:EnableMouse(true); menuClose:Hide()
menuClose:SetScript("OnMouseDown", function() wcMenu:Hide(); menuClose:Hide() end)
wcIcon:SetScript("OnClick", function(self, btn)
    if btn == "LeftButton" then
        wcMenu:Hide(); menuClose:Hide()
        if not mainFrame or not mainFrame:IsShown() then BuildUI() else mainFrame:Hide() end
    elseif btn == "RightButton" then
        if wcMenu:IsShown() then wcMenu:Hide(); menuClose:Hide()
        else lockBtn:Refresh(); hideIconBtn:Refresh(); --animMenuBtn:Refresh();
         mmMenuBtn:Refresh(); PositionMenu(); wcMenu:Show(); menuClose:Show() end
    elseif btn == "MiddleButton" then
        MTP_Toggle()
    end
end)
wcIcon:SetScript("OnMouseWheel", function(self, delta)
    if iconCfg.locked then return end
    iconCfg.size = math.max(ICON_MIN, math.min(ICON_MAX, iconCfg.size + delta*4))
    wcIcon:SetSize(iconCfg.size, iconCfg.size); SaveIconCfg()
end)
wcIcon:SetScript("OnDragStart", function(self)
    if not iconCfg.locked then
        wcIcon:ClearAllPoints()
        wcIcon:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", iconCfg.x, iconCfg.y)
        self:StartMoving()
    end
end)
wcIcon:SetScript("OnDragStop", function(self)
    self:StopMovingOrSizing()
    local left = wcIcon:GetLeft(); local bottom = wcIcon:GetBottom()
    if left and bottom then
        iconCfg.x = left; iconCfg.y = bottom; SaveIconCfg()
    end
    --animClock = 0
end)
wcIcon:SetScript("OnEnter", function(self)
    GameTooltip:SetOwner(self,"ANCHOR_RIGHT"); GameTooltip:ClearLines()
    GameTooltip:AddLine("WeeklyChecklist",1,0.9,0.4); GameTooltip:AddLine(" ")
    GameTooltip:AddLine(WCLL.ttLClick); GameTooltip:AddLine(WCLL.ttRClick)
    GameTooltip:AddLine(WCLL.ttMClick)
    GameTooltip:AddLine(string.format(WCLL.ttWheel, iconCfg.size))
    if not iconCfg.locked then GameTooltip:AddLine(WCLL.ttDrag) end
    GameTooltip:Show()
end)
wcIcon:SetScript("OnLeave", function() GameTooltip:Hide() end)
local function InitIcon()
    WeeklyChecklistDB = WeeklyChecklistDB or {}
    BuildFontList()
    local isFirstInstall = (WeeklyChecklistDB.iconCfg == nil)
    --LoadIconCfg()
    --StopAnim()
    local sw = UIParent:GetWidth()
    local sh = UIParent:GetHeight()
    if sw == 0 or sh == 0 then return end
    if isFirstInstall then
        iconCfg.x = sw / 2 - iconCfg.size / 2
        iconCfg.y = sh / 2 - iconCfg.size / 2
    end
    iconCfg.x = math.max(0, math.min(iconCfg.x, sw - iconCfg.size))
    iconCfg.y = math.max(0, math.min(iconCfg.y, sh - iconCfg.size))
    if isFirstInstall then iconCfg.visible = true end
    SaveIconCfg()
    wcIcon:SetSize(iconCfg.size, iconCfg.size)
    wcIcon:ClearAllPoints()
    wcIcon:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", iconCfg.x, iconCfg.y)
    if iconCfg.visible ~= false then wcIcon:Show() else wcIcon:Hide() end
    local LibDBIcon = LibStub and LibStub("LibDBIcon-1.0", true)
    if LibDBIcon then
        WeeklyChecklistDB.minimap = WeeklyChecklistDB.minimap or {}
        if iconCfg.mmVisible == false then
            WeeklyChecklistDB.minimap.hide = true
        end
        if not LibDBIcon:IsRegistered("WeeklyChecklist") then
            LibDBIcon:Register("WeeklyChecklist", mmBtn, WeeklyChecklistDB.minimap)
            LibDBIcon:RemoveButtonBorder("WeeklyChecklist")
        end
    end
end]]
local iconInitFrame = CreateFrame("Frame")
iconInitFrame:RegisterEvent("ADDON_LOADED")
--iconInitFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
iconInitFrame:SetScript("OnEvent", function(self, event, arg1)
    if event == "ADDON_LOADED" and arg1 == "OrzUI" then
        self:UnregisterEvent("ADDON_LOADED")
        WeeklyChecklistDB = WeeklyChecklistDB or {}
        BuildFontList()
        --LoadIconCfg()
    --elseif event == "PLAYER_ENTERING_WORLD" then
        --InitIcon()
    end
end)
local _addonVersion = "1.49" --C_AddOns.GetAddOnMetadata("WeeklyChecklist", "Version") or "?"
--print("|cFF00FFCCWeeklyChecklist|r  v" .. _addonVersion .. "  command:  |cFFFFD700/wc|r")
--[[local LDB = LibStub and LibStub("LibDataBroker-1.1", true)
if LDB then
    mmBtn = LDB:NewDataObject("WeeklyChecklist", {
        type="launcher", label="WeeklyChecklist",
        tocname="WeeklyChecklist",
        icon="Interface\\AddOns\\OrzUI\\Media\\Hutu\\star",
        OnClick = function(self, btn)
            if btn=="LeftButton" then
                if not mainFrame or not mainFrame:IsShown() then BuildUI() else mainFrame:Hide() end
            elseif btn=="RightButton" then
                if wcIcon then
                    if wcIcon:IsShown() then
                        wcIcon:Hide(); iconCfg.visible = false; SaveIconCfg()
                    else
                        wcIcon:Show(); iconCfg.visible = true;  SaveIconCfg()
                    end
                end
            elseif btn=="MiddleButton" then
                MTP_Toggle()
            end
        end,
        --OnTooltipShow = function(tip)
            --tip:AddLine("WeeklyChecklist",1,0.9,0.4)
            --tip:AddLine(WCLL.ldbLClick); tip:AddLine(WCLL.ldbRClick)
        --end,
    })
end]]

local wc_reset_ev = CreateFrame("Frame")
wc_reset_ev:RegisterEvent("CHAT_MSG_SYSTEM")
wc_reset_ev:SetScript("OnEvent", function(_, event, msg)
    local ok, found = pcall(function() return msg and msg:find("已被重置") end)
    if not ok or not found then return end
    if not IsInGroup() or IsInRaid() then return end
    local ch = IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "PARTY"
    local clean = msg:gsub("|%d([^;]*);[^;]*;", "%1")
    C_Timer.After(0, function() C_ChatInfo.SendChatMessage("[2ui] " .. clean, ch) end)
end)

local WC_WARN_ENCH = {[1]=true,[3]=true,[5]=true,[7]=true,[8]=true,[11]=true,[12]=true,[16]=true}
local WC_SLOTS = {
    {n="CharacterHeadSlot",          id=1,  side="WCLL" },
    {n="CharacterNeckSlot",          id=2,  side="WCLL" },
    {n="CharacterShoulderSlot",      id=3,  side="WCLL" },
    {n="CharacterBackSlot",          id=15, side="WCLL" },
    {n="CharacterChestSlot",         id=5,  side="WCLL" },
    {n="CharacterWristSlot",         id=9,  side="WCLL" },
    {n="CharacterHandsSlot",         id=10, side="R" },
    {n="CharacterWaistSlot",         id=6,  side="R" },
    {n="CharacterLegsSlot",          id=7,  side="R" },
    {n="CharacterFeetSlot",          id=8,  side="R" },
    {n="CharacterFinger0Slot",       id=11, side="R" },
    {n="CharacterFinger1Slot",       id=12, side="R" },
    {n="CharacterTrinket0Slot",      id=13, side="R" },
    {n="CharacterTrinket1Slot",      id=14, side="R" },
    {n="CharacterMainHandSlot",      id=16, side="ML"},
    {n="CharacterSecondaryHandSlot", id=17, side="MR"},
}
local wcOvLabels = {}

local WC_SCAN_NAME = "WCEquipScanTooltip"
local WC_SCAN_TIP = CreateFrame("GameTooltip", WC_SCAN_NAME, WorldFrame, "GameTooltipTemplate")

local WC_EM_DASH  = "\xe2\x80\x93"
local WC_EM_DASH2 = "\xe2\x80\x94"

local function WC_StripEnchantPrefix(s)
    if not s then return "" end
    local icon = s:match("(|A.-|a)$")
    local text = icon and s:sub(1, -(#icon + 1)) or s
    local pos = text:find(WC_EM_DASH, 1, true) or text:find(WC_EM_DASH2, 1, true)
    if pos then
        local after = text:sub(pos + 3):match("^%s*(.-)%s*$")
        return after and (after .. (icon or "")) or s
    end
    local pos2 = text:find(" - ", 1, true)
    if pos2 then
        local after = text:sub(pos2 + 3):match("^%s*(.-)%s*$")
        return after and (after .. (icon or "")) or s
    end
    return (text:match("^%s*(.-)%s*$") or s) .. (icon or "")
end

local function WC_DurColor(pct)
    if pct >= 80 then return 0.7, 0.7, 0.7
    elseif pct >= 50 then return 1, 1, 0
    elseif pct >= 25 then return 1, 0.5, 0
    else return 1, 0.2, 0.2 end
end

local WC_STAT_SHORT = {
    ["暴击"] = "暴击",
    ["全能"] = "全能",
    ["急速"] = "急速",
    ["精通"] = "精通",
    ["主属性"] = "主属性",
}

local function WC_ShortenStat(sname)
    sname = sname:match("^%s*(.-)%s*$") or sname
    if WC_STAT_SHORT[sname] then return WC_STAT_SHORT[sname] end
    for k, v in pairs(WC_STAT_SHORT) do
        if sname:find(k, 1, true) then return v end
    end
    return sname
end

local function WC_ParseSlot(slotID, link)
    local enchDisplay, isEnchanted = nil, false
    local upgradeText = ""
    local gemText = nil

    local itemStr = link:match("|Hitem:([^|]+)|h")
    if itemStr then
        local parts = {}
        for p in itemStr:gmatch("[^:]+") do table.insert(parts, tonumber(p) or 0) end
        isEnchanted = (parts[2] or 0) > 0
    end

    if C_TooltipInfo then
        local ok, data = pcall(C_TooltipInfo.GetInventoryItem, "player", slotID)
        if ok and data and data.lines then
            for _, line in ipairs(data.lines) do
                local t = line.leftText or ""
                if not enchDisplay then
                    local el = ENCHANTED_TOOLTIP_LINE and t:match(ENCHANTED_TOOLTIP_LINE:gsub("%%s", "(.+)"))
                    if el then
                        local cleanEl = el:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")
                        cleanEl = cleanEl:match("^(.-)%s*$") or cleanEl
                        enchDisplay = WC_StripEnchantPrefix(cleanEl)
                    end
                end
                if upgradeText == "" then
                    local cur, max2 = t:match("(%d+)%s*/%s*(%d+)")
                    if cur and max2 and not t:find("耐久度", 1, true) and not t:find("Durability", 1, true) then
                        local track = t:match("^(.-)%s*%d+%s*/%s*%d+")
                        track = track and (track:match(":%s*(.-)%s*$") or track:match("^%s*(.-)%s*$")) or ""
                        track = track:gsub("[:%s]+$", "")
                        upgradeText = (track ~= "" and track or "") .. cur .. "/" .. max2
                    end
                end
            end
            if not enchDisplay and isEnchanted then
                local stats = {}
                for _, line in ipairs(data.lines) do
                    local lc = line.leftColor
                    local t = line.leftText or ""
                    if lc and (lc.g or 0) > 0.7 and (lc.r or 0) < 0.5 and t:match("^%+%d+") then
                        local val, sname = t:match("^%+(%d+) (.+)$")
                    if val and sname and not t:find("镶嵌奖励") then
                            table.insert(stats, sname .. "+" .. val)
                        end
                    end
                end
                if #stats > 0 then enchDisplay = table.concat(stats, " / ") end
            end
        end
    end

    local WC_GEM_SCAN = _G["WCGemScanTooltip"]
    if not WC_GEM_SCAN then
        WC_GEM_SCAN = CreateFrame("GameTooltip", "WCGemScanTooltip", nil, "GameTooltipTemplate")
        for i = 1, 20 do
            local l = _G["WCGemScanTooltipTextLeft"..i]  or WC_GEM_SCAN:CreateFontString("WCGemScanTooltipTextLeft"..i,  "ARTWORK", "GameTooltipText")
            local r = _G["WCGemScanTooltipTextRight"..i] or WC_GEM_SCAN:CreateFontString("WCGemScanTooltipTextRight"..i, "ARTWORK", "GameTooltipText")
            WC_GEM_SCAN:AddFontStrings(l, r)
        end
    end
    WC_GEM_SCAN:SetOwner(UIParent, "ANCHOR_NONE")

    local function WC_ParseStatToken(tok)
        tok = tok:match("^%s*(.-)%s*$") or tok
        tok = tok:gsub("%s*#%d+$", "")
        if tok == "" or not tok:find("+", 1, true) then return nil end
        local sname, val = tok:match("^(.+)%s*%+(%d+)%s*$")
        if not sname then
            val, sname = tok:match("^%+(%d+)%s+(.+)$")
        end
        if val and sname then
            sname = sname:match("^%s*(.-)%s*$") or sname
            return sname .. "+" .. val
        end
        return nil
    end

    local function WC_ParseGemLines(getText, count)
        local stats = {}
        for j = 1, count do
            local gt = getText(j) or ""
            gt = gt:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):match("^%s*(.-)%s*$") or gt
            if gt ~= "" and gt:find("%d+") then
                local skip = gt:find("需要", 1, true) or gt:find("唯一", 1, true) or gt:find("耐久度", 1, true)
                if not skip then
                    local p1, p2 = gt:match("^(.+%+%d+)%s+和%s+(.+%+%d+)%s*$")
                    if p1 and p2 then
                        local s1 = WC_ParseStatToken(p1)
                        local s2 = WC_ParseStatToken(p2)
                        if s1 then table.insert(stats, s1) end
                        if s2 then table.insert(stats, s2) end
                    elseif gt:find("/", 1, true) then
                        for tok in gt:gmatch("[^/]+") do
                            local s = WC_ParseStatToken(tok)
                            if s then table.insert(stats, s) end
                        end
                    else
                        local s = WC_ParseStatToken(gt)
                        if s then table.insert(stats, s) end
                    end
                end
            end
        end
        return stats
    end

    local gemParts = {}
    for i = 1, 3 do
        local _, gLink = C_Item.GetItemGem(link, i)
        if gLink then
            local gID = tonumber(gLink:match("item:(%d+)"))
            if gID then
                local icon = C_Item.GetItemIconByID(gID)
                local iconStr = icon and ("|T" .. tostring(icon) .. ":12:12|t") or ""
                local stats = {}

                WC_GEM_SCAN:ClearLines()
                WC_GEM_SCAN:SetHyperlink(gLink)
                local nLines = WC_GEM_SCAN:NumLines()
                if nLines > 0 then
                    stats = WC_ParseGemLines(function(j)
                        local fs = _G["WCGemScanTooltipTextLeft" .. j]
                        return fs and fs:GetText()
                    end, nLines)
                end

                if #stats == 0 and C_TooltipInfo then
                    local gok, gdata = pcall(C_TooltipInfo.GetItemByID, gID)
                    if gok and gdata and gdata.lines then
                        stats = WC_ParseGemLines(function(j)
                            return gdata.lines[j] and gdata.lines[j].leftText
                        end, #gdata.lines)
                    end
                end

                if #stats > 0 then
                    table.insert(gemParts, iconStr .. table.concat(stats, "\n"))
                end
            end
        end
    end
    if #gemParts > 0 then gemText = table.concat(gemParts, "\n") end

    return enchDisplay, isEnchanted, upgradeText, gemText
end

WC_EquipOverlay_Update = function()
    if not (WeeklyChecklistDB and WeeklyChecklistDB.showEquipInfo) then return end
    for _, entry in ipairs(WC_SLOTS) do
        local btn = _G[entry.n]
        if btn then
            if not wcOvLabels[entry.id] then
                local toRight = (entry.side == "WCLL" or entry.side == "MR")
                local jH      = toRight and "LEFT" or "RIGHT"
                local aSelf   = toRight and "TOPLEFT"  or "TOPRIGHT"
                local aRef    = toRight and "TOPRIGHT" or "TOPLEFT"
                local offX    = toRight and 2 or -2

                local a = btn:CreateFontString(nil, "OVERLAY")
                a:SetFont(STANDARD_TEXT_FONT, 11, "OUTLINE")
                a:SetJustifyH("CENTER")
                a:SetPoint("TOP", btn, "TOP", 0, -1)

                local b = btn:CreateFontString(nil, "OVERLAY")
                b:SetFont(STANDARD_TEXT_FONT, 9, "OUTLINE")
                b:SetJustifyH("CENTER")
                b:SetPoint("TOP", btn, "TOP", 0, -13)

                local c = btn:CreateFontString(nil, "OVERLAY")
                c:SetFont(STANDARD_TEXT_FONT, 11, "OUTLINE")
                local cSelf = toRight and "BOTTOMLEFT" or "BOTTOMRIGHT"
                local cRef  = toRight and "BOTTOMRIGHT" or "BOTTOMLEFT"
                c:SetJustifyH(jH); c:SetWidth(160); c:SetWordWrap(false)
                c:SetPoint(cSelf, btn, cRef, offX, -2)

                local d = btn:CreateFontString(nil, "OVERLAY")
                d:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE")
                d:SetJustifyH(jH); d:SetWidth(220); d:SetWordWrap(false)
                d:SetPoint(aSelf, btn, aRef, offX, -2)

                local d2 = btn:CreateFontString(nil, "OVERLAY")
                d2:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE")
                d2:SetJustifyH(jH); d2:SetWidth(220); d2:SetWordWrap(false)
                d2:SetPoint(aSelf, btn, aRef, offX, -13)

                local e = btn:CreateFontString(nil, "OVERLAY")
                e:SetFont(STANDARD_TEXT_FONT, 9, "OUTLINE")
                e:SetPoint("BOTTOM", btn, "BOTTOM", 0, 2)
                e:SetJustifyH("CENTER")

                wcOvLabels[entry.id] = {a=a, b=b, c=c, d=d, d2=d2, e=e}
            end

            local lbl = wcOvLabels[entry.id]
            local link = GetInventoryItemLink("player", entry.id)
            if link then
                local loc = ItemLocation:CreateFromEquipmentSlot(entry.id)
                local ilvl = C_Item.DoesItemExist(loc) and C_Item.GetCurrentItemLevel(loc)
                local enchDisplay, isEnchanted, upgradeText, gemText = WC_ParseSlot(entry.id, link)

                local ilvlStr = ilvl and tostring(ilvl) or ""
                lbl.a:SetText("|cFFAA44FF" .. ilvlStr .. "|r")
                lbl.a:Show()

                if upgradeText ~= "" then
                    lbl.b:SetText("|cFFFFD700" .. upgradeText .. "|r")
                    lbl.b:Show()
                else
                    lbl.b:Hide()
                end

                if enchDisplay and enchDisplay ~= "" then
                    lbl.c:SetText("|cFF88FF44" .. enchDisplay .. "|r")
                    lbl.c:Show()
                elseif WC_WARN_ENCH[entry.id] and (not enchDisplay or enchDisplay == "") then
                    lbl.c:SetText("|cFFFF4444附魔缺失|r")
                    lbl.c:Show()
                else
                    lbl.c:Hide()
                end

                if gemText then
                    local line1, line2 = gemText:match("^(.-)\n(.+)$")
                    if line1 then
                        lbl.d:SetText("|cFFFFFFFF" .. line1 .. "|r"); lbl.d:Show()
                        lbl.d2:SetText("|cFFFFFFFF" .. line2 .. "|r"); lbl.d2:Show()
                    else
                        lbl.d:SetText("|cFFFFFFFF" .. gemText .. "|r"); lbl.d:Show()
                        lbl.d2:Hide()
                    end
                else
                    lbl.d:Hide(); lbl.d2:Hide()
                end

                local cur, mx = GetInventoryItemDurability(entry.id)
                if cur and mx and mx > 0 then
                    local pct = math.floor(cur / mx * 100)
                    if pct < 100 then
                        local dr, dg, db = WC_DurColor(pct)
                        lbl.e:SetTextColor(dr, dg, db)
                        lbl.e:SetText(pct .. "%")
                        lbl.e:Show()
                    else
                        lbl.e:Hide()
                    end
                else
                    lbl.e:Hide()
                end
            else
                lbl.a:SetText(""); lbl.b:Hide(); lbl.c:Hide(); lbl.d:Hide(); lbl.d2:Hide(); lbl.e:Hide()
            end
        end
    end
end

WC_EquipOverlay_Hide = function()
    for _, lbl in pairs(wcOvLabels) do
        lbl.a:Hide(); lbl.b:Hide(); lbl.c:Hide()
        if lbl.d  then lbl.d:Hide()  end
        if lbl.d2 then lbl.d2:Hide() end
        if lbl.e  then lbl.e:Hide()  end
    end
end

local function WC_Comma(num)
    local n = tostring(math.floor(num))
    local k
    repeat n, k = n:gsub("^(-?%d+)(%d%d%d)", "%1,%2") until k == 0
    return n
end

local function WC_StatHook_Crit(statFrame, unit)
    if not (WeeklyChecklistDB and WeeklyChecklistDB.showEquipInfo) then return end
    if unit ~= "player" then statFrame:Hide() return end
    pcall(function()
        local rating  = GetCombatRating(CR_CRIT_MELEE) or 0
        local percent = math.max(GetSpellCritChance(2) or 0, GetRangedCritChance() or 0, GetCritChance() or 0)
        statFrame.Label:SetText(useEnglish and "Critical Strike:" or "暴击:")
        statFrame.Value:SetText(string.format("%s (%.2f%%)", WC_Comma(rating), percent))
        statFrame:Show()
    end)
end

local function WC_StatHook_Haste(statFrame, unit)
    if not (WeeklyChecklistDB and WeeklyChecklistDB.showEquipInfo) then return end
    if unit ~= "player" then statFrame:Hide() return end
    pcall(function()
        local rating  = GetCombatRating(CR_HASTE_MELEE) or 0
        local percent = GetHaste() or 0
        statFrame.Label:SetText(useEnglish and "Haste:" or "急速:")
        statFrame.Value:SetText(string.format("%s (%.2f%%)", WC_Comma(rating), percent))
        statFrame:Show()
    end)
end

local function WC_StatHook_Mastery(statFrame, unit)
    if not (WeeklyChecklistDB and WeeklyChecklistDB.showEquipInfo) then return end
    if unit ~= "player" then statFrame:Hide() return end
    pcall(function()
        local rating  = GetCombatRating(CR_MASTERY) or 0
        local percent = GetMasteryEffect() or 0
        statFrame.Label:SetText(useEnglish and "Mastery:" or "精通:")
        statFrame.Value:SetText(string.format("%s (%.2f%%)", WC_Comma(rating), percent))
        statFrame:Show()
    end)
end

local function WC_StatHook_Vers(statFrame, unit)
    if not (WeeklyChecklistDB and WeeklyChecklistDB.showEquipInfo) then return end
    if unit ~= "player" then statFrame:Hide() return end
    pcall(function()
        local rating     = GetCombatRating(CR_VERSATILITY_DAMAGE_DONE) or 0
        local percentAtk = GetCombatRatingBonus(CR_VERSATILITY_DAMAGE_DONE) or 0
        local percentDR  = GetCombatRatingBonus(CR_VERSATILITY_DAMAGE_TAKEN) or 0
        statFrame.Label:SetText(useEnglish and "Versatility:" or "全能:")
        statFrame.Value:SetText(string.format("%s (%.2f%%, %.2f%%)", WC_Comma(rating), percentAtk, percentDR))
        statFrame:Show()
    end)
end

local function WC_StatHook_Leech(statFrame, unit)
    if not (WeeklyChecklistDB and WeeklyChecklistDB.showEquipInfo) then return end
    if unit ~= "player" then statFrame:Hide() return end
    pcall(function()
        local rating  = GetCombatRating(CR_LIFESTEAL) or 0
        local percent = GetLifesteal() or 0
        statFrame.Label:SetText(useEnglish and "Leech:" or "吸血:")
        statFrame.Value:SetText(string.format("%s (%.2f%%)", WC_Comma(rating), percent))
        statFrame:Show()
    end)
end

local function WC_StatHook_Avoidance(statFrame, unit)
    if not (WeeklyChecklistDB and WeeklyChecklistDB.showEquipInfo) then return end
    if unit ~= "player" then statFrame:Hide() return end
    pcall(function()
        local rating  = GetCombatRating(CR_AVOIDANCE) or 0
        local percent = GetCombatRatingBonus(CR_AVOIDANCE) or 0
        statFrame.Label:SetText(useEnglish and "Avoidance:" or "闪避:")
        statFrame.Value:SetText(string.format("%s (%.2f%%)", WC_Comma(rating), percent))
        statFrame:Show()
    end)
end

local function WC_StatHook_Speed(statFrame, unit)
    if not (WeeklyChecklistDB and WeeklyChecklistDB.showEquipInfo) then return end
    if unit ~= "player" then statFrame:Hide() return end
    pcall(function()
        local percent = (select(2, GetUnitSpeed("player")) or 0) / 7 * 100
        statFrame.Label:SetText(useEnglish and "Speed:" or "移动速度:")
        statFrame.Value:SetText(string.format("%.2f%%", percent))
        statFrame:Show()
    end)
end

function WC_AvgIlvl_Update()
    if not (WeeklyChecklistDB and WeeklyChecklistDB.showEquipInfo) then return end
    local pane = CharacterStatsPane
    if not pane or not pane.ItemLevelFrame then return end
    local avg, eq = GetAverageItemLevel()
    eq  = math.floor(eq  * 100 + 0.5) / 100
    avg = math.floor(avg * 100 + 0.5) / 100
    if eq == avg then
        pane.ItemLevelFrame.Value:SetFormattedText("%.2f", avg)
    else
        pane.ItemLevelFrame.Value:SetFormattedText("%.2f/%.2f", eq, avg)
    end
end

local wcOvHooked = false
local wcAvgHooked = false
local wcStatHooked = false
local function WC_OvHook()
    if wcOvHooked or not CharacterFrame then return end
    wcOvHooked = true
    CharacterFrame:SetScale(1.08)
    CharacterFrame:HookScript("OnShow", function()
        C_Timer.After(0.1, WC_EquipOverlay_Update)
        C_Timer.After(0.1, WC_AvgIlvl_Update)
    end)
    if not wcAvgHooked and PaperDollFrame_UpdateStats then
        wcAvgHooked = true
        hooksecurefunc("PaperDollFrame_UpdateStats", WC_AvgIlvl_Update)
    end
    if not wcStatHooked then
        wcStatHooked = true
        if PaperDollFrame_SetCritChance  then hooksecurefunc("PaperDollFrame_SetCritChance",  WC_StatHook_Crit)      end
        if PaperDollFrame_SetHaste       then hooksecurefunc("PaperDollFrame_SetHaste",        WC_StatHook_Haste)     end
        if PaperDollFrame_SetMastery     then hooksecurefunc("PaperDollFrame_SetMastery",      WC_StatHook_Mastery)   end
        if PaperDollFrame_SetVersatility then hooksecurefunc("PaperDollFrame_SetVersatility",  WC_StatHook_Vers)      end
        if PaperDollFrame_SetLifesteal   then hooksecurefunc("PaperDollFrame_SetLifesteal",    WC_StatHook_Leech)     end
        if PaperDollFrame_SetAvoidance   then hooksecurefunc("PaperDollFrame_SetAvoidance",    WC_StatHook_Avoidance) end
        if PaperDollFrame_SetSpeed       then hooksecurefunc("PaperDollFrame_SetSpeed",        WC_StatHook_Speed)     end
    end
    if CharacterFrame:IsShown() then
        C_Timer.After(0.1, WC_EquipOverlay_Update)
        C_Timer.After(0.1, WC_AvgIlvl_Update)
    end
end

--[[local wcOvEvt = CreateFrame("Frame")
--wcOvEvt:RegisterEvent("ADDON_LOADED")
--wcOvEvt:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
--wcOvEvt:RegisterEvent("PLAYER_ENTERING_WORLD")
wcOvEvt:SetScript("OnEvent", function(self, event, arg1)
    if event == "ADDON_LOADED" then
        --if arg1 == "Blizzard_CharacterFrame" then
            --WC_OvHook()
        --end
        if arg1 == "OrzUI" then
            WeeklyChecklistDB = WeeklyChecklistDB or {}
            if WeeklyChecklistDB.showEquipInfo == nil then
                WeeklyChecklistDB.showEquipInfo = true
            end
            --if C_AddOns.IsAddOnLoaded("Blizzard_CharacterFrame") then
                --WC_OvHook()
            --end
        end
    --elseif event == "PLAYER_ENTERING_WORLD" then
        --C_Timer.After(1, function()
            --C_AddOns.LoadAddOn("Blizzard_CharacterFrame")
            --WC_OvHook()
        --end)
    --elseif event == "PLAYER_EQUIPMENT_CHANGED" then
        --if CharacterFrame and CharacterFrame:IsShown() then
            --C_Timer.After(0.2, WC_EquipOverlay_Update)
            --C_Timer.After(0.2, WC_AvgIlvl_Update)
        --end
    end
end)]]
