commit 2a55dd6dada0f79276a20b3c678ce18b7f973329 Author: rucky Date: Mon Mar 16 13:48:46 2026 +0800 init diff --git a/.nanami-launcher.json b/.nanami-launcher.json new file mode 100644 index 0000000..ba9d80a --- /dev/null +++ b/.nanami-launcher.json @@ -0,0 +1,7 @@ +{ + "packageId": "nanami-ui", + "packageName": "Nanami-UI", + "source": "bundled", + "folderName": "Nanami-UI", + "installedAt": "2026-03-04T11:12:15.763Z" +} diff --git a/AFKScreen.lua b/AFKScreen.lua new file mode 100644 index 0000000..2c330d0 --- /dev/null +++ b/AFKScreen.lua @@ -0,0 +1,1529 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: AFK Screen (AFKScreen.lua) +-- Full-screen idle screen with dancing character model & info panel +-------------------------------------------------------------------------------- + +SFrames.AFKScreen = {} +local AFK = SFrames.AFKScreen + +local FADE_SPEED = 1.25 +local PARTICLE_COUNT = 18 +local CLOCK_UPDATE_INTERVAL = 0.5 + +local CLASS_NAMES_ZH = { + ["WARRIOR"] = "战士", ["MAGE"] = "法师", ["ROGUE"] = "盗贼", + ["DRUID"] = "德鲁伊", ["HUNTER"] = "猎人", ["SHAMAN"] = "萨满", + ["PRIEST"] = "牧师", ["WARLOCK"] = "术士", ["PALADIN"] = "圣骑士", +} + +local T = SFrames.ActiveTheme + +local WORLD_BUFF_DEFS = { + ["Rallying Cry of the Dragonslayer"] = { display = "屠龙者的咆哮 (龙头)", priority = 1, color = {1, 0.85, 0.30} }, + ["屠龙者的咆哮"] = { display = "屠龙者的咆哮 (龙头)", priority = 1, color = {1, 0.85, 0.30} }, + ["巨龙杀手的战吼"] = { display = "屠龙者的咆哮 (龙头)", priority = 1, color = {1, 0.85, 0.30} }, + + ["Warchief's Blessing"] = { display = "酋长的祝福", priority = 2, color = {0.90, 0.55, 0.30} }, + ["酋长的祝福"] = { display = "酋长的祝福", priority = 2, color = {0.90, 0.55, 0.30} }, + + ["Spirit of Zandalar"] = { display = "赞达拉之魂", priority = 2, color = {0.40, 0.90, 0.45} }, + ["赞达拉之魂"] = { display = "赞达拉之魂", priority = 2, color = {0.40, 0.90, 0.45} }, + + ["Songflower Serenade"] = { display = "歌唱花小夜曲", priority = 3, color = {0.75, 0.55, 0.90} }, + ["歌唱花小夜曲"] = { display = "歌唱花小夜曲", priority = 3, color = {0.75, 0.55, 0.90} }, + + ["Mol'dar's Moxie"] = { display = "莫达尔的勇气", priority = 3, color = {0.50, 0.80, 0.55} }, + ["莫达尔的勇气"] = { display = "莫达尔的勇气", priority = 3, color = {0.50, 0.80, 0.55} }, + ["Fengus' Ferocity"] = { display = "芬古斯的凶猛", priority = 3, color = {0.85, 0.55, 0.40} }, + ["芬古斯的凶猛"] = { display = "芬古斯的凶猛", priority = 3, color = {0.85, 0.55, 0.40} }, + ["Slip'kik's Savvy"] = { display = "斯里基克的精明", priority = 3, color = {0.55, 0.70, 0.90} }, + ["斯里基克的精明"] = { display = "斯里基克的精明", priority = 3, color = {0.55, 0.70, 0.90} }, + + ["Darkmoon Faire"] = { display = "暗月马戏团", priority = 4, color = {0.65, 0.50, 0.80} }, +} + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- + +local function NextName(prefix) + if not AFK._nameCounter then AFK._nameCounter = 0 end + AFK._nameCounter = AFK._nameCounter + 1 + return "NanamiAFK_" .. prefix .. AFK._nameCounter +end + +local function GetFont() + return SFrames:GetFont() +end + +local function GetOutline() + return SFrames.Media.fontOutline or "OUTLINE" +end + +local function GetClassColor(class) + local c = SFrames.Config.colors.class[class] + if c then return c.r, c.g, c.b end + return 1, 1, 1 +end + +local function FormatDuration(seconds) + local h = math.floor(seconds / 3600) + local m = math.floor((seconds - h * 3600) / 60) + local s = math.floor(seconds - h * 3600 - m * 60) + if h > 0 then + return string.format("%d:%02d:%02d", h, m, s) + else + return string.format("%02d:%02d", m, s) + end +end + +local function GetDurabilityPercent() + local current, maximum = 0, 0 + for slot = 1, 18 do + local ok, cur, mx = pcall(function() + local hasItem = GetInventoryItemLink("player", slot) + if hasItem then + local c, m = GetInventoryItemDurability(slot) + return c, m + end + return nil, nil + end) + if ok and cur and mx and mx > 0 then + current = current + cur + maximum = maximum + mx + end + end + if maximum == 0 then return nil end + return math.floor((current / maximum) * 100) +end + +local function GetPvPRankName() + local ok, name = pcall(function() return GetPVPRankInfo(UnitPVPRank("player")) end) + if ok and name and name ~= "" then return name end + return nil +end + +-------------------------------------------------------------------------------- +-- UI Construction +-------------------------------------------------------------------------------- + +function AFK:Build() + if self.frame then return end + + local f = CreateFrame("Frame", "NanamiAFKScreen", WorldFrame) + f:SetFrameStrata("FULLSCREEN_DIALOG") + f:SetFrameLevel(100) + f:SetAllPoints(UIParent) + f:EnableMouse(true) + f:EnableKeyboard(true) + f:SetAlpha(0) + f:Hide() + self.frame = f + + -- Full-screen dark overlay + f.overlay = f:CreateTexture(nil, "BACKGROUND") + f.overlay:SetTexture("Interface\\Buttons\\WHITE8X8") + f.overlay:SetAllPoints(f) + f.overlay:SetVertexColor(T.overlayBg[1], T.overlayBg[2], T.overlayBg[3]) + f.overlay:SetAlpha(T.overlayBg[4]) + + -- Top gradient (subtle vignette) + f.topGrad = f:CreateTexture(nil, "BACKGROUND", nil, 1) + f.topGrad:SetTexture("Interface\\Buttons\\WHITE8X8") + f.topGrad:SetPoint("TOPLEFT", f, "TOPLEFT") + f.topGrad:SetPoint("TOPRIGHT", f, "TOPRIGHT") + f.topGrad:SetHeight(200) + f.topGrad:SetGradientAlpha("VERTICAL", 0, 0, 0, 0, 0.02, 0.005, 0.02, 0.6) + + -- Bottom gradient + f.botGrad = f:CreateTexture(nil, "BACKGROUND", nil, 1) + f.botGrad:SetTexture("Interface\\Buttons\\WHITE8X8") + f.botGrad:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT") + f.botGrad:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT") + f.botGrad:SetHeight(250) + f.botGrad:SetGradientAlpha("VERTICAL", 0.03, 0.01, 0.03, 0.45, 0, 0, 0, 0) + + -- Bottom accent line + f.accentLine = f:CreateTexture(nil, "ARTWORK") + f.accentLine:SetTexture("Interface\\Buttons\\WHITE8X8") + f.accentLine:SetHeight(1) + f.accentLine:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 60) + f.accentLine:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 60) + f.accentLine:SetVertexColor(T.accentLine[1], T.accentLine[2], T.accentLine[3]) + f.accentLine:SetAlpha(T.accentLine[4]) + + -- Accent glow (soft line above the accent) + f.accentGlow = f:CreateTexture(nil, "ARTWORK", nil, -1) + f.accentGlow:SetTexture("Interface\\Buttons\\WHITE8X8") + f.accentGlow:SetHeight(30) + f.accentGlow:SetPoint("BOTTOMLEFT", f.accentLine, "TOPLEFT", 0, 0) + f.accentGlow:SetPoint("BOTTOMRIGHT", f.accentLine, "TOPRIGHT", 0, 0) + f.accentGlow:SetGradientAlpha("VERTICAL", + T.accentLine[1], T.accentLine[2], T.accentLine[3], 0.18, + T.accentLine[1], T.accentLine[2], T.accentLine[3], 0) + + self:BuildModel(f) + self:BuildInfoPanel(f) + self:BuildStatsPanel(f) + self:BuildSkillsPanel(f) + self:BuildWorldBuffPanel(f) + self:BuildClock(f) + self:BuildParticles(f) + self:BuildBrand(f) + + -- Exit hint below accent line + local hintFs = f:CreateFontString(nil, "OVERLAY") + hintFs:SetFont(GetFont(), 11, GetOutline()) + hintFs:SetPoint("BOTTOM", f, "BOTTOM", 0, 22) + hintFs:SetJustifyH("CENTER") + hintFs:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3]) + hintFs:SetText("- 点击或按任意键退出 -") + self.hintText = hintFs + + -- Exit AFK on key press or mouse click + f:SetScript("OnKeyDown", function() + if AFK.isShowing and not AFK._exiting then + AFK:RequestExit() + end + end) + f:SetScript("OnMouseDown", function() + if AFK.isShowing and not AFK._exiting then + AFK:RequestExit() + end + end) + + -- Master OnUpdate + f:SetScript("OnUpdate", function() + AFK:OnUpdate(arg1) + end) +end + +-------------------------------------------------------------------------------- +-- 3D Character Model +-------------------------------------------------------------------------------- + +function AFK:BuildModel(parent) + local model = CreateFrame("PlayerModel", NextName("Model"), parent) + model:SetWidth(420) + model:SetHeight(520) + model:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 30, 70) + model:SetFrameLevel(parent:GetFrameLevel() + 1) + + -- Soft darkening on the right side so the info panel is readable + local modelFade = parent:CreateTexture(nil, "ARTWORK", nil, 0) + modelFade:SetTexture("Interface\\Buttons\\WHITE8X8") + modelFade:SetPoint("TOPRIGHT", parent, "TOPRIGHT") + modelFade:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT") + modelFade:SetWidth(420) + modelFade:SetGradientAlpha("HORIZONTAL", + 0, 0, 0, 0, + T.overlayBg[1], T.overlayBg[2], T.overlayBg[3], 0.65) + + self.model = model + self.modelFacing = 0 +end + +-------------------------------------------------------------------------------- +-- Info Panel (bottom-right area above accent line) +-------------------------------------------------------------------------------- + +function AFK:BuildInfoPanel(parent) + local panel = CreateFrame("Frame", nil, parent) + panel:SetFrameLevel(parent:GetFrameLevel() + 5) + panel:SetWidth(210) + panel:SetHeight(150) + panel:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -40, 75) + self.infoPanel = panel + + panel:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = false, tileSize = 0, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + panel:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + panel:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) + + local yOff = -10 + local xPad = 12 + + local classIconFrame = CreateFrame("Frame", nil, panel) + classIconFrame:SetFrameLevel(panel:GetFrameLevel() + 1) + classIconFrame:SetWidth(36) + classIconFrame:SetHeight(36) + classIconFrame:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad, yOff) + local classIcon = classIconFrame:CreateTexture(nil, "OVERLAY") + classIcon:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\UI-Classes-Circles") + classIcon:SetAllPoints(classIconFrame) + classIcon:Hide() + self.classIcon = classIcon + self.classIconFrame = classIconFrame + + local nameFs = panel:CreateFontString(nil, "OVERLAY") + nameFs:SetFont(GetFont(), 16, GetOutline()) + nameFs:SetPoint("TOPLEFT", classIconFrame, "TOPRIGHT", 8, 0) + nameFs:SetJustifyH("LEFT") + self.nameText = nameFs + + local classFs = panel:CreateFontString(nil, "OVERLAY") + classFs:SetFont(GetFont(), 10, GetOutline()) + classFs:SetPoint("TOPLEFT", nameFs, "BOTTOMLEFT", 0, -2) + classFs:SetJustifyH("LEFT") + classFs:SetTextColor(T.valueColor[1], T.valueColor[2], T.valueColor[3]) + self.classText = classFs + + local sep1 = panel:CreateTexture(nil, "ARTWORK") + sep1:SetTexture("Interface\\Buttons\\WHITE8X8") + sep1:SetHeight(1) + sep1:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad, yOff - 44) + sep1:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -xPad, yOff - 44) + sep1:SetVertexColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3]) + sep1:SetAlpha(0.35) + + local rowStart = yOff - 52 + local rowGap = 15 + + local function MakeRow(label, yPos, iconKey) + if iconKey and SFrames and SFrames.CreateIcon then + local ico = SFrames:CreateIcon(panel, iconKey, 9) + ico:SetDrawLayer("OVERLAY") + ico:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad, yPos) + ico:SetVertexColor(T.labelColor[1], T.labelColor[2], T.labelColor[3]) + ico:SetAlpha(0.85) + end + + local lbl = panel:CreateFontString(nil, "OVERLAY") + lbl:SetFont(GetFont(), 9, GetOutline()) + lbl:SetPoint("TOPLEFT", panel, "TOPLEFT", iconKey and (xPad + 13) or xPad, yPos) + lbl:SetJustifyH("LEFT") + lbl:SetTextColor(T.labelColor[1], T.labelColor[2], T.labelColor[3]) + lbl:SetText(label) + + local val = panel:CreateFontString(nil, "OVERLAY") + val:SetFont(GetFont(), 9, GetOutline()) + val:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -xPad, yPos) + val:SetJustifyH("RIGHT") + val:SetTextColor(T.valueColor[1], T.valueColor[2], T.valueColor[3]) + return val + end + + self.guildText = MakeRow("公会", rowStart, "party") + self.zoneText = MakeRow("位置", rowStart - rowGap, "worldmap") + self.durText = MakeRow("耐久度", rowStart - rowGap * 2, "tank") + self.pvpText = MakeRow("军衔", rowStart - rowGap * 3, "honor") + self.playtimeText = MakeRow("游戏时长", rowStart - rowGap * 4, "hearthstone") +end + +-------------------------------------------------------------------------------- +-- Stats Panel (top-right area — character attributes) +-------------------------------------------------------------------------------- + +function AFK:BuildStatsPanel(parent) + local panel = CreateFrame("Frame", nil, parent) + panel:SetFrameLevel(parent:GetFrameLevel() + 5) + panel:SetWidth(210) + panel:SetHeight(230) + panel:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -40, -80) + self.statsPanel = panel + + panel:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = false, tileSize = 0, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + panel:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], 0.72) + panel:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.45) + + local statsIco = SFrames:CreateIcon(panel, "charsheet", 12) + statsIco:SetDrawLayer("OVERLAY") + statsIco:SetPoint("TOP", panel, "TOP", -38, -8) + statsIco:SetVertexColor(T.titleColor[1], T.titleColor[2], T.titleColor[3]) + + local titleFs = panel:CreateFontString(nil, "OVERLAY") + titleFs:SetFont(GetFont(), 11, GetOutline()) + titleFs:SetPoint("LEFT", statsIco, "RIGHT", 4, 0) + titleFs:SetJustifyH("CENTER") + titleFs:SetTextColor(T.titleColor[1], T.titleColor[2], T.titleColor[3]) + titleFs:SetText("- 角色属性 -") + + local sep = panel:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetHeight(1) + sep:SetPoint("TOPLEFT", panel, "TOPLEFT", 12, -22) + sep:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -22) + sep:SetVertexColor(T.accentLine[1], T.accentLine[2], T.accentLine[3]) + sep:SetAlpha(0.3) + + local xPad = 14 + local yStart = -30 + local rowGap = 15 + self._statRows = {} + + local statDefs = { + { key = "hp", label = "生命值" }, + { key = "mp", label = "法力值" }, + { key = "armor", label = "护甲" }, + { divider = true }, + { key = "str", label = "力量" }, + { key = "agi", label = "敏捷" }, + { key = "sta", label = "耐力" }, + { key = "int", label = "智力" }, + { key = "spi", label = "精神" }, + { divider = true }, + { key = "melee", label = "攻击强度" }, + { key = "spell", label = "法术强度" }, + { key = "crit", label = "暴击率" }, + { key = "dodge", label = "躲闪" }, + } + + local curY = yStart + for _, def in ipairs(statDefs) do + if def.divider then + local div = panel:CreateTexture(nil, "ARTWORK") + div:SetTexture("Interface\\Buttons\\WHITE8X8") + div:SetHeight(1) + div:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad + 4, curY - 2) + div:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -xPad - 4, curY - 2) + div:SetVertexColor(0.45, 0.28, 0.40) + div:SetAlpha(0.25) + curY = curY - 8 + else + local lbl = panel:CreateFontString(nil, "OVERLAY") + lbl:SetFont(GetFont(), 9, GetOutline()) + lbl:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad, curY) + lbl:SetJustifyH("LEFT") + lbl:SetTextColor(0.58, 0.48, 0.55) + lbl:SetText(def.label) + + local val = panel:CreateFontString(nil, "OVERLAY") + val:SetFont(GetFont(), 10, GetOutline()) + val:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -xPad, curY) + val:SetJustifyH("RIGHT") + val:SetTextColor(0.92, 0.84, 0.90) + + self._statRows[def.key] = val + curY = curY - rowGap + end + end +end + +-------------------------------------------------------------------------------- +-- Skills Panel (right side, below stats — professions & weapon skills) +-------------------------------------------------------------------------------- + +function AFK:BuildSkillsPanel(parent) + local panel = CreateFrame("Frame", nil, parent) + panel:SetFrameLevel(parent:GetFrameLevel() + 5) + panel:SetWidth(210) + panel:SetHeight(200) + panel:SetPoint("TOPRIGHT", self.statsPanel, "BOTTOMRIGHT", 0, -6) + self.skillsPanel = panel + + panel:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = false, tileSize = 0, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + panel:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], 0.72) + panel:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.45) + + local skillsIco = SFrames:CreateIcon(panel, "profession", 12) + skillsIco:SetDrawLayer("OVERLAY") + skillsIco:SetPoint("TOP", panel, "TOP", -38, -8) + skillsIco:SetVertexColor(T.titleColor[1], T.titleColor[2], T.titleColor[3]) + + local titleFs = panel:CreateFontString(nil, "OVERLAY") + titleFs:SetFont(GetFont(), 11, GetOutline()) + titleFs:SetPoint("LEFT", skillsIco, "RIGHT", 4, 0) + titleFs:SetJustifyH("CENTER") + titleFs:SetTextColor(T.titleColor[1], T.titleColor[2], T.titleColor[3]) + titleFs:SetText("- 技能总览 -") + + local sep = panel:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetHeight(1) + sep:SetPoint("TOPLEFT", panel, "TOPLEFT", 12, -22) + sep:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -12, -22) + sep:SetVertexColor(T.accentLine[1], T.accentLine[2], T.accentLine[3]) + sep:SetAlpha(0.3) + + self._skillBars = {} +end + +function AFK:CreateSkillBar(parent, yOffset, name, rank, maxRank, barColor) + local xPad = 14 + local barWidth = 182 + local barHeight = 8 + + local lbl = parent:CreateFontString(nil, "OVERLAY") + lbl:SetFont(GetFont(), 8, GetOutline()) + lbl:SetPoint("TOPLEFT", parent, "TOPLEFT", xPad, yOffset) + lbl:SetJustifyH("LEFT") + lbl:SetTextColor(0.62, 0.52, 0.58) + lbl:SetText(name) + + local valFs = parent:CreateFontString(nil, "OVERLAY") + valFs:SetFont(GetFont(), 8, GetOutline()) + valFs:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -xPad, yOffset) + valFs:SetJustifyH("RIGHT") + valFs:SetTextColor(0.82, 0.74, 0.80) + if maxRank and maxRank > 0 then + valFs:SetText(rank .. " / " .. maxRank) + else + valFs:SetText(rank) + end + + local bgBar = parent:CreateTexture(nil, "ARTWORK") + bgBar:SetTexture("Interface\\Buttons\\WHITE8X8") + bgBar:SetWidth(barWidth) + bgBar:SetHeight(barHeight) + bgBar:SetPoint("TOPLEFT", parent, "TOPLEFT", xPad, yOffset - 10) + bgBar:SetVertexColor(T.panelBg[1], T.panelBg[2], T.panelBg[3]) + bgBar:SetAlpha(0.85) + + -- Bar fill + local fillBar = parent:CreateTexture(nil, "OVERLAY") + fillBar:SetTexture("Interface\\Buttons\\WHITE8X8") + fillBar:SetHeight(barHeight) + fillBar:SetPoint("TOPLEFT", bgBar, "TOPLEFT", 0, 0) + + local pct = 0 + if maxRank and maxRank > 0 then + pct = rank / maxRank + elseif rank and rank > 0 then + pct = 1 + end + local fillWidth = math.max(1, barWidth * pct) + fillBar:SetWidth(fillWidth) + + local cr, cg, cb = barColor[1], barColor[2], barColor[3] + fillBar:SetVertexColor(cr, cg, cb, 0.75) + + -- Subtle highlight on bar top + local shine = parent:CreateTexture(nil, "OVERLAY", nil, 1) + shine:SetTexture("Interface\\Buttons\\WHITE8X8") + shine:SetHeight(math.floor(barHeight / 2)) + shine:SetPoint("TOPLEFT", fillBar, "TOPLEFT", 0, 0) + shine:SetPoint("TOPRIGHT", fillBar, "TOPRIGHT", 0, 0) + shine:SetVertexColor(1, 1, 1, 0.08) + + return { lbl = lbl, val = valFs, bg = bgBar, fill = fillBar, shine = shine } +end + +-------------------------------------------------------------------------------- +-- World Buff Panel (center — prominent buff reminders) +-------------------------------------------------------------------------------- + +function AFK:BuildWorldBuffPanel(parent) + local panel = CreateFrame("Frame", nil, parent) + panel:SetFrameLevel(parent:GetFrameLevel() + 7) + panel:SetWidth(320) + panel:SetHeight(60) + panel:SetPoint("CENTER", parent, "CENTER", -50, 30) + self.wbPanel = panel + panel:Hide() + + panel:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = false, tileSize = 0, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + panel:SetBackdropColor(0.06, 0.04, 0.01, 0.82) + panel:SetBackdropBorderColor(T.wbBorder[1], T.wbBorder[2], T.wbBorder[3], 0.55) + + local glow = CreateFrame("Frame", nil, parent) + glow:SetFrameLevel(parent:GetFrameLevel() + 6) + glow:SetWidth(328) + glow:SetHeight(68) + glow:SetPoint("CENTER", panel, "CENTER", 0, 0) + glow:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = false, tileSize = 0, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + glow:SetBackdropColor(0, 0, 0, 0) + glow:SetBackdropBorderColor(1, 0.82, 0.30, 0.20) + glow:Hide() + self._wbGlow = glow + + local titleFs = panel:CreateFontString(nil, "OVERLAY") + titleFs:SetFont(GetFont(), 12, GetOutline()) + titleFs:SetPoint("TOP", panel, "TOP", 0, -10) + titleFs:SetJustifyH("CENTER") + titleFs:SetTextColor(T.wbGold[1], T.wbGold[2], T.wbGold[3]) + titleFs:SetText("⚔ 世界增益 ⚔") + self.wbTitle = titleFs + + local sep = panel:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetHeight(1) + sep:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, -26) + sep:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -14, -26) + sep:SetVertexColor(1, 0.78, 0.30, 0.35) + + self._wbRows = {} + self._wbPulse = 0 +end + +-------------------------------------------------------------------------------- +-- Clock & AFK Timer (top center) +-------------------------------------------------------------------------------- + +function AFK:BuildClock(parent) + local clockFrame = CreateFrame("Frame", nil, parent) + clockFrame:SetFrameLevel(parent:GetFrameLevel() + 5) + clockFrame:SetWidth(400) + clockFrame:SetHeight(150) + clockFrame:SetPoint("TOP", parent, "TOP", 0, -30) + self.clockFrame = clockFrame + + -- Main clock: local time (large) + local timeFs = clockFrame:CreateFontString(nil, "OVERLAY") + timeFs:SetFont(GetFont(), 72, GetOutline()) + timeFs:SetPoint("TOP", clockFrame, "TOP", 0, 0) + timeFs:SetJustifyH("CENTER") + timeFs:SetTextColor(T.clockColor[1], T.clockColor[2], T.clockColor[3]) + self.clockText = timeFs + + -- Sub clock: server time (below main) + local serverFs = clockFrame:CreateFontString(nil, "OVERLAY") + serverFs:SetFont(GetFont(), 18, GetOutline()) + serverFs:SetPoint("TOP", timeFs, "BOTTOM", 0, -4) + serverFs:SetJustifyH("CENTER") + serverFs:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3]) + self.serverClockText = serverFs + + -- AFK timer + local timerFs = clockFrame:CreateFontString(nil, "OVERLAY") + timerFs:SetFont(GetFont(), 18, GetOutline()) + timerFs:SetPoint("TOP", serverFs, "BOTTOM", 0, -8) + timerFs:SetJustifyH("CENTER") + timerFs:SetTextColor(T.timerColor[1], T.timerColor[2], T.timerColor[3]) + self.timerText = timerFs + + -- Subtle decorative dots flanking the main clock + for _, side in ipairs({ -1, 1 }) do + local dot = clockFrame:CreateTexture(nil, "ARTWORK") + dot:SetTexture("Interface\\Buttons\\WHITE8X8") + dot:SetWidth(4) + dot:SetHeight(4) + dot:SetPoint("CENTER", timeFs, side > 0 and "RIGHT" or "LEFT", side * 18, 0) + dot:SetVertexColor(T.accentLine[1], T.accentLine[2], T.accentLine[3]) + dot:SetAlpha(0.5) + end +end + +-------------------------------------------------------------------------------- +-- Floating Particles (sakura / firefly effect) +-------------------------------------------------------------------------------- + +function AFK:BuildParticles(parent) + self.particles = {} + for i = 1, PARTICLE_COUNT do + local p = parent:CreateTexture(nil, "ARTWORK", nil, 2) + p:SetTexture("Interface\\Buttons\\WHITE8X8") + local size = math.random(2, 6) + p:SetWidth(size) + p:SetHeight(size) + p:SetVertexColor( + T.particleColor[1] + math.random() * 0.1 - 0.05, + T.particleColor[2] + math.random() * 0.2 - 0.1, + T.particleColor[3] + math.random() * 0.1 - 0.05 + ) + p:SetAlpha(0) + + p._baseAlpha = 0.15 + math.random() * 0.25 + p._speed = 8 + math.random() * 20 + p._drift = (math.random() - 0.5) * 40 + p._phase = math.random() * 6.28 + p._pulseSpeed = 1.5 + math.random() * 2 + p._x = math.random() * 1024 + p._y = math.random() * 768 + + p:SetPoint("CENTER", parent, "BOTTOMLEFT", p._x, p._y) + table.insert(self.particles, p) + end +end + +-------------------------------------------------------------------------------- +-- Brand / Logo (bottom-left) +-------------------------------------------------------------------------------- + +function AFK:BuildBrand(parent) + local brandIco = SFrames:CreateIcon(parent, "logo", 28) + brandIco:SetDrawLayer("OVERLAY") + brandIco:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 20, 30) + brandIco:SetVertexColor(T.brandColor[1], T.brandColor[2], T.brandColor[3]) + brandIco:SetAlpha(0.85) + self.brandIcon = brandIco + + local brandFs = parent:CreateFontString(nil, "OVERLAY") + brandFs:SetFont(GetFont(), 16, GetOutline()) + brandFs:SetPoint("LEFT", brandIco, "RIGHT", 6, 2) + brandFs:SetJustifyH("LEFT") + brandFs:SetTextColor(T.brandColor[1], T.brandColor[2], T.brandColor[3]) + brandFs:SetAlpha(0.85) + brandFs:SetText("Nanami-UI v1.0.0") + self.brandText = brandFs + + local catFs = parent:CreateFontString(nil, "OVERLAY") + catFs:SetFont(GetFont(), 12, GetOutline()) + catFs:SetPoint("TOPLEFT", brandIco, "BOTTOMLEFT", 0, -3) + catFs:SetJustifyH("LEFT") + catFs:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3]) + catFs:SetAlpha(0.7) + catFs:SetText("=^_^= Meow~") + self.catText = catFs +end + +-------------------------------------------------------------------------------- +-- Data Refresh +-------------------------------------------------------------------------------- + +function AFK:RefreshInfo() + local name = UnitName("player") or "Unknown" + local level = UnitLevel("player") or 0 + local _, classEn = UnitClass("player") + classEn = classEn or "WARRIOR" + + local cr, cg, cb = GetClassColor(classEn) + self.nameText:SetText(name) + self.nameText:SetTextColor(cr, cg, cb) + + local className = CLASS_NAMES_ZH[classEn] or UnitClass("player") or classEn + self.classText:SetText("Lv." .. level .. " " .. className) + + -- Class icon + local coords = SFrames.CLASS_ICON_TCOORDS and SFrames.CLASS_ICON_TCOORDS[classEn] + if coords then + self.classIcon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]) + self.classIcon:Show() + self.classIconFrame:Show() + else + self.classIcon:Hide() + end + + -- Guild + local guildName, guildRank = GetGuildInfo("player") + if guildName then + self.guildText:SetText("<" .. guildName .. "> " .. (guildRank or "")) + else + self.guildText:SetText("-") + self.guildText:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3]) + end + + -- Zone + local zone = GetZoneText() or "" + local subZone = GetSubZoneText() or "" + if subZone ~= "" and subZone ~= zone then + self.zoneText:SetText(zone .. " - " .. subZone) + else + self.zoneText:SetText(zone) + end + + -- Durability + local dur = GetDurabilityPercent() + if dur then + local dr, dg, db = 0.4, 1, 0.4 + if dur < 25 then + dr, dg, db = 1, 0.2, 0.2 + elseif dur < 50 then + dr, dg, db = 1, 0.7, 0.2 + end + self.durText:SetText(dur .. "%") + self.durText:SetTextColor(dr, dg, db) + else + self.durText:SetText("-") + self.durText:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3]) + end + + -- PvP Rank + local pvpRank = GetPvPRankName() + if pvpRank then + self.pvpText:SetText(pvpRank) + else + self.pvpText:SetText("-") + self.pvpText:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3]) + end + + -- Playtime (request it; the event callback fills it in) + self.playtimeText:SetText("...") + self.playtimeText:SetTextColor(T.dimColor[1], T.dimColor[2], T.dimColor[3]) + self._waitingPlaytime = true + RequestTimePlayed() + + -- Character stats, skills & world buffs + self:RefreshStats() + self:RefreshSkills() + self:RefreshWorldBuffs() +end + +function AFK:RefreshStats() + if not self._statRows then return end + local R = self._statRows + + -- HP / MP + local hp = UnitHealthMax("player") or 0 + local mp = UnitManaMax("player") or 0 + R.hp:SetText(hp) + R.mp:SetText(mp) + + local _, pClass = UnitClass("player") + if pClass == "WARRIOR" or pClass == "ROGUE" then + R.mp:SetTextColor(0.50, 0.42, 0.48) + end + + -- Armor + local base, effectiveArmor = UnitArmor("player") + R.armor:SetText(effectiveArmor or base or 0) + + -- Primary stats + local function GetStat(id) + local base, stat, posBuff, negBuff = UnitStat("player", id) + return stat or base or 0 + end + R.str:SetText(GetStat(1)) + R.agi:SetText(GetStat(2)) + R.sta:SetText(GetStat(3)) + R.int:SetText(GetStat(4)) + R.spi:SetText(GetStat(5)) + + -- Melee attack power + local mBase, mPos, mNeg = UnitAttackPower("player") + local ap = (mBase or 0) + (mPos or 0) + (mNeg or 0) + R.melee:SetText(ap) + + -- Spell power (approximate from bonus healing / damage stats if available) + local sp = 0 + if GetSpellBonusDamage then + for school = 2, 7 do + local v = GetSpellBonusDamage(school) + if v and v > sp then sp = v end + end + end + R.spell:SetText(sp) + + -- Crit chance + local crit = 0 + if GetCritChance then crit = GetCritChance() or 0 + elseif GetMeleeCritChance then crit = GetMeleeCritChance() or 0 + end + R.crit:SetText(string.format("%.1f%%", crit)) + + -- Dodge + local dodge = GetDodgeChance and GetDodgeChance() or 0 + R.dodge:SetText(string.format("%.1f%%", dodge)) +end + +function AFK:RefreshSkills() + if not self.skillsPanel then return end + + -- Clean up old bars + if self._skillBars then + for _, bar in ipairs(self._skillBars) do + if bar.lbl then bar.lbl:Hide() end + if bar.val then bar.val:Hide() end + if bar.bg then bar.bg:Hide() end + if bar.fill then bar.fill:Hide() end + if bar.shine then bar.shine:Hide() end + end + end + self._skillBars = {} + + -- Also clean up section labels + if self._skillLabels then + for _, fs in ipairs(self._skillLabels) do fs:Hide() end + end + self._skillLabels = {} + + local tradeSkills = {} + local secondarySkills = {} + local weapons = {} + local numSkills = GetNumSkillLines and GetNumSkillLines() or 0 + local currentHeader = "" + + for i = 1, numSkills do + local name, header, isExpanded, skillRank, numTemp, skillMod, skillMax, isAbandonable = GetSkillLineInfo(i) + if header then + currentHeader = name or "" + elseif name then + local isWeapon = (currentHeader == "Weapon Skills" or currentHeader == "武器技能" + or currentHeader == "Weapons" or currentHeader == "Combat") + local isTrade = (currentHeader == "Trade Skills" or currentHeader == "专业技能" + or currentHeader == "Professions") + local isSecondary = (currentHeader == "Secondary Skills" or currentHeader == "辅助技能") + + if isWeapon and skillMax and skillMax > 0 then + table.insert(weapons, { name = name, rank = skillRank or 0, max = skillMax }) + elseif isTrade and skillMax and skillMax > 0 then + table.insert(tradeSkills, { name = name, rank = skillRank or 0, max = skillMax }) + elseif isSecondary and skillMax and skillMax > 0 then + table.insert(secondarySkills, { name = name, rank = skillRank or 0, max = skillMax }) + end + end + end + + local professions = {} + local maxProf = 3 + for _, p in ipairs(tradeSkills) do + if table.getn(professions) < maxProf then table.insert(professions, p) end + end + for _, p in ipairs(secondarySkills) do + if table.getn(professions) < maxProf then table.insert(professions, p) end + end + + local panel = self.skillsPanel + local yOff = -28 + local barSpacing = 22 + + local profColor = { 0.45, 0.75, 0.90 } + local weapColor = { 0.85, 0.60, 0.45 } + + local function AddSectionLabel(text, y) + local fs = panel:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 9, GetOutline()) + fs:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, y) + fs:SetJustifyH("LEFT") + fs:SetTextColor(T.titleColor[1], T.titleColor[2], T.titleColor[3]) + fs:SetAlpha(0.85) + fs:SetText(text) + table.insert(self._skillLabels, fs) + end + + if table.getn(professions) > 0 then + AddSectionLabel("专业技能", yOff) + yOff = yOff - 14 + for _, p in ipairs(professions) do + local bar = self:CreateSkillBar(panel, yOff, p.name, p.rank, p.max, profColor) + table.insert(self._skillBars, bar) + yOff = yOff - barSpacing + end + yOff = yOff - 4 + end + + -- Sort weapons by rank descending; show top 4 + table.sort(weapons, function(a, b) return a.rank > b.rank end) + local maxWeapons = 4 + if table.getn(weapons) > maxWeapons then + local trimmed = {} + for i = 1, maxWeapons do table.insert(trimmed, weapons[i]) end + weapons = trimmed + end + + if table.getn(weapons) > 0 then + AddSectionLabel("武器熟练", yOff) + yOff = yOff - 14 + for _, w in ipairs(weapons) do + local bar = self:CreateSkillBar(panel, yOff, w.name, w.rank, w.max, weapColor) + table.insert(self._skillBars, bar) + yOff = yOff - barSpacing + end + end + + local totalHeight = math.abs(yOff) + 10 + if totalHeight < 50 then totalHeight = 50 end + panel:SetHeight(totalHeight) +end + +-------------------------------------------------------------------------------- +-- World Buff Scanning & Refresh +-------------------------------------------------------------------------------- + +-- Texture-based fallback for world buff detection (server-independent) +local WORLD_BUFF_TEXTURES = { + ["Interface\\Icons\\Spell_Holy_GreaterHeal"] = { display = "屠龙者的咆哮 (龙头)", priority = 1, color = {1, 0.85, 0.30} }, + ["Interface\\Icons\\INV_Misc_Head_Dragon_01"] = { display = "屠龙者的咆哮 (龙头)", priority = 1, color = {1, 0.85, 0.30} }, + ["Interface\\Icons\\Spell_Arcane_TeleportOrgrimmar"] = { display = "酋长的祝福", priority = 2, color = {0.90, 0.55, 0.30} }, + ["Interface\\Icons\\Ability_Creature_Poison_05"] = { display = "赞达拉之魂", priority = 2, color = {0.40, 0.90, 0.45} }, + ["Interface\\Icons\\Spell_Holy_MindSoothe"] = { display = "歌唱花小夜曲", priority = 3, color = {0.75, 0.55, 0.90} }, +} + +function AFK:ScanWorldBuffs() + local found = {} + local matched = {} + + if not self._buffTip then + self._buffTip = CreateFrame("GameTooltip", "NanamiAFKBuffTip", WorldFrame, "GameTooltipTemplate") + end + + -- Build texture → remaining time mapping + local texTime = {} + if GetPlayerBuff then + for slot = 0, 31 do + local bi = GetPlayerBuff(slot, "HELPFUL") + if bi and bi >= 0 then + local tex = GetPlayerBuffTexture and GetPlayerBuffTexture(bi) + local tl = GetPlayerBuffTimeLeft and GetPlayerBuffTimeLeft(bi) + if tex then texTime[tex] = tl or 0 end + end + end + end + + -- Debug: print all buffs once per AFK session to help identify names + local debugOnce = not self._wbDebugDone + if debugOnce then self._wbDebugDone = true end + + for i = 1, 32 do + local tex = UnitBuff("player", i) + if not tex then break end + + self._buffTip:SetOwner(WorldFrame, "ANCHOR_NONE") + self._buffTip:SetUnitBuff("player", i) + local bName = NanamiAFKBuffTipTextLeft1 and NanamiAFKBuffTipTextLeft1:GetText() + self._buffTip:Hide() + + if debugOnce and bName then + local tl = texTime[tex] or 0 + if tl > 300 then + DEFAULT_CHAT_FRAME:AddMessage("|cff88aacc[NanamiAFK Debug] buff: \"" .. bName .. "\" tex: " .. tex .. " time: " .. math.floor(tl) .. "s|r") + end + end + + local def = nil + -- 1) Exact name match + if bName then + def = WORLD_BUFF_DEFS[bName] + end + -- 2) Partial name match (substring) + if not def and bName then + for pattern, d in pairs(WORLD_BUFF_DEFS) do + if string.find(bName, pattern, 1, true) then + def = d + break + end + end + end + -- 3) Texture-based fallback + if not def and tex then + def = WORLD_BUFF_TEXTURES[tex] + end + + if def and not matched[def.display] then + matched[def.display] = true + table.insert(found, { + name = def.display, + timeLeft = texTime[tex] or 0, + priority = def.priority, + color = def.color, + texture = tex, + }) + end + end + + table.sort(found, function(a, b) return a.priority < b.priority end) + return found +end + +function AFK:RefreshWorldBuffs() + if not self.wbPanel then return end + + for _, row in ipairs(self._wbRows or {}) do + if row.icon then row.icon:Hide() end + if row.nameFs then row.nameFs:Hide() end + if row.timeFs then row.timeFs:Hide() end + if row.highlight then row.highlight:Hide() end + end + self._wbRows = {} + + local buffs = self:ScanWorldBuffs() + + if table.getn(buffs) == 0 then + self.wbPanel:Hide() + if self._wbGlow then self._wbGlow:Hide() end + return + end + + local panel = self.wbPanel + local yOff = -34 + local rowHeight = 28 + local xPad = 16 + + for _, buff in ipairs(buffs) do + local isPrimary = (buff.priority == 1) + local nameSize = isPrimary and 13 or 11 + local timeSize = isPrimary and 15 or 12 + + local icon = panel:CreateFontString(nil, "OVERLAY") + icon:SetFont(GetFont(), isPrimary and 14 or 11, GetOutline()) + icon:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad, yOff) + icon:SetTextColor(buff.color[1], buff.color[2], buff.color[3]) + icon:SetText(isPrimary and "★" or "◆") + + local nameFs = panel:CreateFontString(nil, "OVERLAY") + nameFs:SetFont(GetFont(), nameSize, GetOutline()) + nameFs:SetPoint("TOPLEFT", panel, "TOPLEFT", xPad + 18, yOff) + nameFs:SetJustifyH("LEFT") + nameFs:SetTextColor(buff.color[1], buff.color[2], buff.color[3]) + nameFs:SetText(buff.name) + + local timeFs = panel:CreateFontString(nil, "OVERLAY") + timeFs:SetFont(GetFont(), timeSize, GetOutline()) + timeFs:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -xPad, yOff) + timeFs:SetJustifyH("RIGHT") + self:_SetBuffTimeColor(timeFs, buff.timeLeft) + + local highlight = nil + if isPrimary then + highlight = panel:CreateTexture(nil, "ARTWORK") + highlight:SetTexture("Interface\\Buttons\\WHITE8X8") + highlight:SetHeight(rowHeight - 4) + highlight:SetPoint("TOPLEFT", panel, "TOPLEFT", 6, yOff + 3) + highlight:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -6, yOff + 3) + highlight:SetVertexColor(1, 0.82, 0.25, 0.06) + end + + table.insert(self._wbRows, { + icon = icon, nameFs = nameFs, timeFs = timeFs, + highlight = highlight, buff = buff, + }) + yOff = yOff - rowHeight + end + + local totalH = math.abs(yOff) + 10 + if totalH < 60 then totalH = 60 end + panel:SetHeight(totalH) + panel:Show() + + if self._wbGlow then + self._wbGlow:SetHeight(totalH + 8) + self._wbGlow:Show() + end +end + +function AFK:_SetBuffTimeColor(fs, timeLeft) + if timeLeft and timeLeft > 0 then + fs:SetText(FormatDuration(timeLeft)) + if timeLeft < 600 then + fs:SetTextColor(1, 0.35, 0.30) + elseif timeLeft < 1800 then + fs:SetTextColor(1, 0.72, 0.30) + else + fs:SetTextColor(0.92, 0.88, 0.72) + end + else + fs:SetText("--:--") + fs:SetTextColor(0.50, 0.46, 0.38) + end +end + +function AFK:UpdateWorldBuffTimers() + if not self._wbRows or table.getn(self._wbRows) == 0 then return end + + local texTime = {} + if GetPlayerBuff then + for slot = 0, 31 do + local bi = GetPlayerBuff(slot, "HELPFUL") + if bi and bi >= 0 then + local tex = GetPlayerBuffTexture and GetPlayerBuffTexture(bi) + local tl = GetPlayerBuffTimeLeft and GetPlayerBuffTimeLeft(bi) + if tex then texTime[tex] = tl or 0 end + end + end + end + + for _, row in ipairs(self._wbRows) do + if row.buff and row.buff.texture then + local tl = texTime[row.buff.texture] + if tl and tl > 0 then + self:_SetBuffTimeColor(row.timeFs, tl) + if row.nameFs then row.nameFs:SetAlpha(1) end + if row.icon then row.icon:SetAlpha(1) end + elseif tl == nil then + row.timeFs:SetText("|cff665e50已消失|r") + row.timeFs:SetTextColor(0.42, 0.38, 0.32) + if row.nameFs then row.nameFs:SetAlpha(0.45) end + if row.icon then row.icon:SetAlpha(0.45) end + else + self:_SetBuffTimeColor(row.timeFs, 0) + end + end + end +end + +-------------------------------------------------------------------------------- +-- OnUpdate (master tick) +-------------------------------------------------------------------------------- + +function AFK:OnUpdate(elapsed) + if not elapsed then return end + + -- Fade logic + if self.fadeDirection then + local alpha = self.frame:GetAlpha() + alpha = alpha + self.fadeDirection * FADE_SPEED * elapsed + if self.fadeDirection > 0 and alpha >= 1 then + alpha = 1 + self.fadeDirection = nil + elseif self.fadeDirection < 0 and alpha <= 0 then + alpha = 0 + self.fadeDirection = nil + self.frame:Hide() + self.frame:EnableKeyboard(false) + self.isShowing = false + self._exiting = false + UIParent:Show() + return + end + self.frame:SetAlpha(alpha) + end + + if not self.isShowing then return end + + -- Dance initialization: apply SetSequence once after model is loaded + if self._danceWait then + self._danceWait = self._danceWait - elapsed + if self._danceWait <= 0 then + self._danceWait = nil + if self.model and self.model.SetSequence then + self.model:SetSequence(69) + end + end + end + + -- Clock & timer + self._clockElapsed = (self._clockElapsed or 0) + elapsed + if self._clockElapsed >= CLOCK_UPDATE_INTERVAL then + self._clockElapsed = 0 + + -- Main clock: local time + local localTime = date("%H:%M") + self.clockText:SetText(localTime) + + -- Sub clock: server time + local sHour, sMin = GetGameTime() + self.serverClockText:SetText("服务器 " .. string.format("%02d:%02d", sHour, sMin)) + + -- AFK duration + if self.afkStartTime then + local duration = GetTime() - self.afkStartTime + self.timerText:SetText("已离开 " .. FormatDuration(duration)) + end + + -- World buff timers + self:UpdateWorldBuffTimers() + end + + -- Particle animation + local now = GetTime() + local sw = self.frame:GetWidth() + local sh = self.frame:GetHeight() + if sw < 100 then sw = 1024 end + if sh < 100 then sh = 768 end + + for _, p in ipairs(self.particles) do + p._y = p._y + p._speed * elapsed + p._x = p._x + p._drift * elapsed + + if p._y > sh + 20 then + p._y = -20 + p._x = math.random() * sw + end + if p._x < -20 then p._x = sw + 10 end + if p._x > sw + 20 then p._x = -10 end + + p:ClearAllPoints() + p:SetPoint("CENTER", self.frame, "BOTTOMLEFT", p._x, p._y) + + local pulse = p._baseAlpha * (0.6 + 0.4 * math.sin(now * p._pulseSpeed + p._phase)) + p:SetAlpha(pulse) + end + + -- Accent line gentle pulse + if self.frame.accentLine then + local glow = 0.7 + 0.3 * math.sin(now * 1.2) + self.frame.accentLine:SetAlpha(glow * T.accentLine[4]) + end + + -- World buff panel glow pulse + if self._wbGlow and self._wbGlow:IsShown() then + self._wbPulse = (self._wbPulse or 0) + elapsed * 1.8 + local pa = 0.18 + 0.12 * math.sin(self._wbPulse) + self._wbGlow:SetBackdropBorderColor(1, 0.82, 0.30, pa) + end + + -- Hint text gentle pulse + if self.hintText then + local hAlpha = 0.4 + 0.25 * math.sin(now * 1.8) + self.hintText:SetAlpha(hAlpha) + end +end + +-------------------------------------------------------------------------------- +-- Show / Hide +-------------------------------------------------------------------------------- + +function AFK:Show() + if self.isShowing then return end + if self._exiting then return end + + -- Check config: is AFK screen enabled? + if SFramesDB and SFramesDB.afkEnabled == false then return end + + -- Only trigger in resting areas unless config allows everywhere, or manual toggle + if not self._manualTrigger then + local allowOutside = SFramesDB and SFramesDB.afkOutsideRest == true + if not allowOutside and not IsResting() then return end + end + + if not self.frame then + self:Build() + end + + self.isShowing = true + self._exiting = false + self._manualTrigger = false + self._wbDebugDone = false + self.afkStartTime = GetTime() + self._clockElapsed = 99 + + self.model:SetUnit("player") + self.model:SetCamera(1) + self.model:SetPosition(0, 0, 0) + self.model:SetFacing(0.4) + + -- WoW 1.12 AnimationData.dbc: 69 = EmoteDance; apply after model loads + self._danceWait = 0.3 + + -- Reset particles + local sw = UIParent:GetWidth() or 1024 + local sh = UIParent:GetHeight() or 768 + for _, p in ipairs(self.particles) do + p._x = math.random() * sw + p._y = math.random() * sh + end + + self:RefreshInfo() + + -- Hide the game UI (our frame is parented to WorldFrame, unaffected) + UIParent:Hide() + + -- Fade in + self.frame:SetAlpha(0) + self.frame:EnableKeyboard(true) + self.frame:Show() + self.fadeDirection = 1 +end + +function AFK:Hide() + if not self.isShowing then return end + if self._exiting then return end + self._exiting = true + self.fadeDirection = -1 + self.frame:EnableKeyboard(false) + self._danceWait = nil +end + +function AFK:ForceHide() + if self.frame then + self.frame:SetAlpha(0) + self.frame:Hide() + self.frame:EnableKeyboard(false) + end + self.isShowing = false + self.fadeDirection = nil + self._exiting = false + UIParent:Show() +end + +function AFK:RequestExit() + if self._exiting then return end + self:Hide() + self._lastActivity = GetTime() + self._isAFK = false +end + +function AFK:Toggle() + if self.isShowing then + self:RequestExit() + else + self._manualTrigger = true + self:Show() + end +end + +-------------------------------------------------------------------------------- +-- Event Handling +-------------------------------------------------------------------------------- + +function AFK:ResetIdleTimer() + self._lastActivity = GetTime() + if self.isShowing and not self._exiting then + self:RequestExit() + end +end + +function AFK:OnSystemMessage(msg) + if not msg then return end + local isNowAFK = false + if MARKED_AFK_MESSAGE then + local pattern = string.gsub(MARKED_AFK_MESSAGE, "%%s", ".+") + if string.find(msg, pattern) then isNowAFK = true end + end + if not isNowAFK and string.find(msg, "You are now AFK") then isNowAFK = true end + if not isNowAFK and string.find(msg, "暂离") then isNowAFK = true end + + if isNowAFK then + self._isAFK = true + end + + -- Detect leaving AFK: reset flag + local isNoLongerAFK = false + if CLEARED_AFK_MESSAGE and string.find(msg, CLEARED_AFK_MESSAGE, 1, true) then + isNoLongerAFK = true + end + if not isNoLongerAFK and string.find(msg, "no longer AFK") then isNoLongerAFK = true end + if not isNoLongerAFK and string.find(msg, "取消") and string.find(msg, "暂离") then isNoLongerAFK = true end + if isNoLongerAFK then + self._isAFK = false + end +end + +function AFK:OnTimePlayed(totalTime, levelTime) + if not self._waitingPlaytime then return end + self._waitingPlaytime = false + + if not self.playtimeText then return end + if not totalTime or totalTime == 0 then + self.playtimeText:SetText("-") + return + end + + local days = math.floor(totalTime / 86400) + local hours = math.floor((totalTime - days * 86400) / 3600) + local mins = math.floor((totalTime - days * 86400 - hours * 3600) / 60) + + local text = "" + if days > 0 then + text = days .. "天 " .. hours .. "小时" + elseif hours > 0 then + text = hours .. "小时 " .. mins .. "分钟" + else + text = mins .. "分钟" + end + self.playtimeText:SetText(text) + self.playtimeText:SetTextColor(T.valueColor[1], T.valueColor[2], T.valueColor[3]) +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- + +function AFK:Initialize() + self:Build() + self._isAFK = false + self._lastActivity = GetTime() + + local function MarkActive() + AFK._lastActivity = GetTime() + end + + -- Hook action bar usage + local origUseAction = UseAction + UseAction = function(a1, a2, a3) + MarkActive() + return origUseAction(a1, a2, a3) + end + + if CastSpellByName then + local origCast = CastSpellByName + CastSpellByName = function(a1, a2) + MarkActive() + return origCast(a1, a2) + end + end + + if JumpOrAscendStart then + local origJump = JumpOrAscendStart + JumpOrAscendStart = function() + MarkActive() + return origJump() + end + end + + -- Events that indicate LOCAL player activity (not other players) + local activityEvents = { + "PLAYER_STARTED_MOVING", "PLAYER_STOPPED_MOVING", + "SPELLCAST_START", "SPELLCAST_STOP", + "PLAYER_REGEN_DISABLED", "PLAYER_TARGET_CHANGED", + "LOOT_OPENED", "MERCHANT_SHOW", "BANKFRAME_OPENED", + "MAIL_SHOW", "QUEST_DETAIL", "GOSSIP_SHOW", + "TRADE_SHOW", "AUCTION_HOUSE_SHOW", + } + for _, ev in ipairs(activityEvents) do + SFrames:RegisterEvent(ev, function() AFK:ResetIdleTimer() end) + end + + -- Watcher frame: tracks cursor movement + checks idle time + local watcher = CreateFrame("Frame", "NanamiAFKWatcher", UIParent) + watcher._checkTimer = 0 + watcher._lastCursorX = 0 + watcher._lastCursorY = 0 + watcher:SetScript("OnUpdate", function() + this._checkTimer = (this._checkTimer or 0) + arg1 + if this._checkTimer < 1 then return end + this._checkTimer = 0 + + -- Detect mouse cursor movement (catches all mouse activity) + local cx, cy = GetCursorPosition() + if cx ~= this._lastCursorX or cy ~= this._lastCursorY then + this._lastCursorX = cx + this._lastCursorY = cy + AFK._lastActivity = GetTime() + if AFK.isShowing and not AFK._exiting then + AFK:RequestExit() + end + return + end + + if AFK.isShowing or AFK._exiting then return end + if SFramesDB and SFramesDB.afkEnabled == false then return end + + local delay = (SFramesDB and SFramesDB.afkDelay) or 5 + if delay <= 0 then delay = 0.01 end + + local idle = GetTime() - (AFK._lastActivity or GetTime()) + if idle >= delay * 60 then + local allowOutside = SFramesDB and SFramesDB.afkOutsideRest == true + if allowOutside or IsResting() then + AFK:Show() + end + end + end) + + -- Server AFK message as secondary instant trigger + SFrames:RegisterEvent("CHAT_MSG_SYSTEM", function() + AFK:OnSystemMessage(arg1) + end) + + SFrames:RegisterEvent("TIME_PLAYED_MSG", function() + AFK:OnTimePlayed(arg1, arg2) + end) + + SFrames:RegisterEvent("ZONE_CHANGED_NEW_AREA", function() + if AFK.isShowing and AFK.zoneText then + local zone = GetZoneText() or "" + local subZone = GetSubZoneText() or "" + if subZone ~= "" and subZone ~= zone then + AFK.zoneText:SetText(zone .. " - " .. subZone) + else + AFK.zoneText:SetText(zone) + end + end + end) + + SFrames:RegisterEvent("ZONE_CHANGED", function() + if AFK.isShowing and AFK.zoneText then + local zone = GetZoneText() or "" + local subZone = GetSubZoneText() or "" + if subZone ~= "" and subZone ~= zone then + AFK.zoneText:SetText(zone .. " - " .. subZone) + else + AFK.zoneText:SetText(zone) + end + end + end) +end diff --git a/ActionBars.lua b/ActionBars.lua new file mode 100644 index 0000000..d6638db --- /dev/null +++ b/ActionBars.lua @@ -0,0 +1,1749 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: ActionBars +-- +-- DESIGN: Blizzard's ActionButton_GetPagedID() identifies action slots by +-- checking button:GetParent():GetName(). We MUST NOT reparent multi-bar +-- buttons. Instead we reposition the original bar FRAMES and style buttons +-- in-place. Only ActionButton1-12 are safely reparented (page-based calc). +-- +-- ShapeshiftBarFrame and PetActionBarFrame are children of MainMenuBar, so +-- they become invisible when we hide MainMenuBar. We reparent them to +-- UIParent before hiding. +-------------------------------------------------------------------------------- + +SFrames.ActionBars = {} + +local AB = SFrames.ActionBars + +local DEFAULTS = { + enable = true, + buttonSize = 36, + buttonGap = 2, + smallBarSize = 27, + scale = 1.0, + barCount = 3, + showHotkey = true, + showMacroName = false, + rangeColoring = true, + showPetBar = true, + showStanceBar = true, + showRightBars = true, + alwaysShowGrid = false, + buttonRounded = false, + buttonInnerShadow = false, + hideGryphon = true, + gryphonStyle = "dragonflight", + gryphonOnTop = false, + gryphonWidth = 64, + gryphonHeight = 64, + gryphonOffsetX = 30, + gryphonOffsetY = 0, + bottomOffsetX = 0, + bottomOffsetY = 2, + rightOffsetX = -4, + rightOffsetY = -80, +} + +local BUTTONS_PER_ROW = 12 + +-- 狮鹫样式定义:每种样式包含联盟和部落纹理路径 +local GRYPHON_STYLES = { + { key = "dragonflight", label = "巨龙时代", + alliance = "Interface\\AddOns\\Nanami-UI\\img\\df-gryphon", + horde = "Interface\\AddOns\\Nanami-UI\\img\\df-wyvern" }, + { key = "dragonflight_beta", label = "巨龙时代 (Beta)", + alliance = "Interface\\AddOns\\Nanami-UI\\img\\df-gryphon-beta", + horde = "Interface\\AddOns\\Nanami-UI\\img\\df-gryphon-beta" }, + { key = "classic", label = "经典", + alliance = "Interface\\MainMenuBar\\UI-MainMenuBar-EndCap-Human", + horde = "Interface\\MainMenuBar\\UI-MainMenuBar-EndCap-Human" }, + { key = "cat", label = "猫", + alliance = "Interface\\AddOns\\Nanami-UI\\img\\cat", + horde = "Interface\\AddOns\\Nanami-UI\\img\\cat" }, +} + +local function GetGryphonTexPath(styleKey) + local faction = UnitFactionGroup and UnitFactionGroup("player") or "Alliance" + for _, s in ipairs(GRYPHON_STYLES) do + if s.key == styleKey then + return (faction == "Horde") and s.horde or s.alliance + end + end + return GRYPHON_STYLES[1].alliance +end + +-------------------------------------------------------------------------------- +-- Layout helpers +-------------------------------------------------------------------------------- +local function LayoutRow(buttons, parent, size, gap) + for i, b in ipairs(buttons) do + b:SetWidth(size) + b:SetHeight(size) + b:ClearAllPoints() + if i == 1 then + b:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 0, 0) + else + b:SetPoint("LEFT", buttons[i - 1], "RIGHT", gap, 0) + end + end +end + +local function LayoutColumn(buttons, parent, size, gap) + for i, b in ipairs(buttons) do + b:SetWidth(size) + b:SetHeight(size) + b:ClearAllPoints() + if i == 1 then + b:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, 0) + else + b:SetPoint("TOP", buttons[i - 1], "BOTTOM", 0, -gap) + end + end +end + +-------------------------------------------------------------------------------- +-- DB +-------------------------------------------------------------------------------- +function AB:GetDB() + if not SFramesDB then SFramesDB = {} end + if type(SFramesDB.ActionBars) ~= "table" then SFramesDB.ActionBars = {} end + local db = SFramesDB.ActionBars + for k, v in pairs(DEFAULTS) do + if db[k] == nil then db[k] = v end + end + return db +end + + +-------------------------------------------------------------------------------- +-- Style helpers +-------------------------------------------------------------------------------- +local styledButtons = {} + +local function HideNormalTexture(nt) + if not nt then return end + if nt.SetAlpha then nt:SetAlpha(0) end + if nt.SetWidth then nt:SetWidth(0) end + if nt.SetHeight then nt:SetHeight(0) end +end + +local function StyleButton(b) + if not b or styledButtons[b] then return end + styledButtons[b] = true + + local nt = _G[b:GetName() .. "NormalTexture"] + HideNormalTexture(nt) + b.SetNormalTexture = function() end + + -- pfUI approach: backdrop on a SEPARATE child frame at lower frame level + -- so it renders behind the button's own textures (Icon etc.) + if b:GetBackdrop() then b:SetBackdrop(nil) end + local level = b:GetFrameLevel() + local bd = CreateFrame("Frame", nil, b) + bd:SetFrameLevel(level > 0 and (level - 1) or 0) + bd:SetAllPoints(b) + SFrames:CreateBackdrop(bd) + b.sfBackdrop = bd + + local icon = _G[b:GetName() .. "Icon"] + if icon then + icon:ClearAllPoints() + icon:SetPoint("TOPLEFT", b, "TOPLEFT", 2, -2) + icon:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2) + icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + end + + local cd = _G[b:GetName() .. "Cooldown"] + if cd then + cd:ClearAllPoints() + cd:SetPoint("TOPLEFT", b, "TOPLEFT", 2, -2) + cd:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2) + end + + local hotkey = _G[b:GetName() .. "HotKey"] + if hotkey then + hotkey:SetFont(SFrames:GetFont(), 9, "OUTLINE") + hotkey:ClearAllPoints() + hotkey:SetPoint("TOPRIGHT", b, "TOPRIGHT", -2, -2) + end + + local count = _G[b:GetName() .. "Count"] + if count then + count:SetFont(SFrames:GetFont(), 9, "OUTLINE") + count:ClearAllPoints() + count:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2) + end + + local macroName = _G[b:GetName() .. "Name"] + if macroName then + macroName:SetFont(SFrames:GetFont(), 8, "OUTLINE") + macroName:ClearAllPoints() + macroName:SetPoint("BOTTOM", b, "BOTTOM", 0, 2) + end + + local floatingBG = _G[b:GetName() .. "FloatingBG"] + if floatingBG then floatingBG:SetAlpha(0) end + + local border = _G[b:GetName() .. "Border"] + if border then border:SetAlpha(0) end +end + +local function KillPetNormalTextures(b) + local name = b:GetName() + -- Pet buttons use NormalTexture AND NormalTexture2 + for _, suffix in ipairs({"NormalTexture", "NormalTexture2"}) do + local nt = _G[name .. suffix] + if nt and nt.SetTexture then nt:SetTexture(nil) end + if nt and nt.SetAlpha then nt:SetAlpha(0) end + if nt and nt.Hide then nt:Hide() end + end + local gnt = b.GetNormalTexture and b:GetNormalTexture() + if gnt and gnt.SetTexture then gnt:SetTexture(nil) end + if gnt and gnt.SetAlpha then gnt:SetAlpha(0) end +end + +local function StylePetButton(b) + if not b or styledButtons[b] then return end + styledButtons[b] = true + + KillPetNormalTextures(b) + b.SetNormalTexture = function() end + + -- pfUI approach: backdrop on separate child frame at lower frame level + if b.GetBackdrop and b:GetBackdrop() then b:SetBackdrop(nil) end + local level = b:GetFrameLevel() + local bd = CreateFrame("Frame", nil, b) + bd:SetFrameLevel(level > 0 and (level - 1) or 0) + bd:SetAllPoints(b) + SFrames:CreateBackdrop(bd) + b.sfBackdrop = bd + + local icon = _G[b:GetName() .. "Icon"] + if icon then + icon:ClearAllPoints() + icon:SetPoint("TOPLEFT", b, "TOPLEFT", 2, -2) + icon:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2) + icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + end + + local cd = _G[b:GetName() .. "Cooldown"] + if cd then + cd:ClearAllPoints() + cd:SetPoint("TOPLEFT", b, "TOPLEFT", 2, -2) + cd:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2) + end + + local ab = _G[b:GetName() .. "AutoCastable"] + if ab then + ab:ClearAllPoints() + ab:SetPoint("TOPLEFT", b, "TOPLEFT", -4, 4) + ab:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 4, -4) + end + + local floatingBG = _G[b:GetName() .. "FloatingBG"] + if floatingBG then floatingBG:SetAlpha(0) end +end + + +-------------------------------------------------------------------------------- +-- Button visual effects (rounded corners + inner shadow) +-------------------------------------------------------------------------------- +local function CreateInnerShadow(btn) + if btn.sfInnerShadow then return btn.sfInnerShadow end + local shadow = {} + local thickness = 4 + + local top = btn:CreateTexture(nil, "OVERLAY") + top:SetTexture("Interface\\Buttons\\WHITE8X8") + top:SetHeight(thickness) + top:SetGradientAlpha("VERTICAL", 0, 0, 0, 0, 0, 0, 0, 0.5) + shadow.top = top + + local bot = btn:CreateTexture(nil, "OVERLAY") + bot:SetTexture("Interface\\Buttons\\WHITE8X8") + bot:SetHeight(thickness) + bot:SetGradientAlpha("VERTICAL", 0, 0, 0, 0.5, 0, 0, 0, 0) + shadow.bottom = bot + + local left = btn:CreateTexture(nil, "OVERLAY") + left:SetTexture("Interface\\Buttons\\WHITE8X8") + left:SetWidth(thickness) + left:SetGradientAlpha("HORIZONTAL", 0, 0, 0, 0.5, 0, 0, 0, 0) + shadow.left = left + + local right = btn:CreateTexture(nil, "OVERLAY") + right:SetTexture("Interface\\Buttons\\WHITE8X8") + right:SetWidth(thickness) + right:SetGradientAlpha("HORIZONTAL", 0, 0, 0, 0, 0, 0, 0, 0.5) + shadow.right = right + + btn.sfInnerShadow = shadow + return shadow +end + +local function ApplyButtonVisuals(btn, rounded, shadow) + local bd = btn.sfBackdrop + if not bd then return end + + local inset = rounded and 3 or 2 + btn.sfIconInset = inset + + if rounded then + bd:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + else + bd:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 } + }) + end + local A = SFrames.ActiveTheme + if A and A.panelBg then + bd:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.9) + bd:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1) + else + bd:SetBackdropColor(0.1, 0.1, 0.1, 0.9) + bd:SetBackdropBorderColor(0, 0, 0, 1) + end + + local name = btn:GetName() + if name then + local icon = _G[name .. "Icon"] + if icon then + icon:ClearAllPoints() + icon:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) + icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) + end + local cd = _G[name .. "Cooldown"] + if cd then + cd:ClearAllPoints() + cd:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) + cd:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) + end + end + + if btn.sfRangeOverlay then + btn.sfRangeOverlay:ClearAllPoints() + btn.sfRangeOverlay:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) + btn.sfRangeOverlay:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) + end + + if shadow then + if not btn.sfInnerShadow then CreateInnerShadow(btn) end + local s = btn.sfInnerShadow + s.top:ClearAllPoints() + s.top:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) + s.top:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -inset, -inset) + s.bottom:ClearAllPoints() + s.bottom:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", inset, inset) + s.bottom:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) + s.left:ClearAllPoints() + s.left:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) + s.left:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", inset, inset) + s.right:ClearAllPoints() + s.right:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -inset, -inset) + s.right:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) + s.top:Show(); s.bottom:Show(); s.left:Show(); s.right:Show() + else + if btn.sfInnerShadow then + local s = btn.sfInnerShadow + s.top:Hide(); s.bottom:Hide(); s.left:Hide(); s.right:Hide() + end + end +end + + +-------------------------------------------------------------------------------- +-- Hide Blizzard chrome +-------------------------------------------------------------------------------- +function AB:HideBlizzardBars() + SHOW_MULTI_ACTIONBAR_1 = 1 + SHOW_MULTI_ACTIONBAR_2 = 1 + SHOW_MULTI_ACTIONBAR_3 = 1 + SHOW_MULTI_ACTIONBAR_4 = 1 + + -- Reparent stance/pet bars BEFORE hiding MainMenuBar (they are children of it) + if ShapeshiftBarFrame then ShapeshiftBarFrame:SetParent(UIParent) end + if PetActionBarFrame then PetActionBarFrame:SetParent(UIParent) end + + if MultiActionBar_Update then + MultiActionBar_Update() + end + + if MainMenuBar then + MainMenuBar:UnregisterAllEvents() + MainMenuBar:Hide() + MainMenuBar.Show = function() end + end + + local hideArt = { + "MainMenuBarArtFrame", + "MainMenuExpBar", + "ReputationWatchBar", + } + for _, name in ipairs(hideArt) do + local f = _G[name] + if f then + f:Hide() + if f.SetHeight then f:SetHeight(0) end + f.Show = function() end + end + end + + -- BonusActionBarFrame: keep functional for druid form switching, just hide art + if BonusActionBarFrame then + BonusActionBarFrame:SetParent(UIParent) + if BonusActionBarFrame.SetBackdrop then + BonusActionBarFrame:SetBackdrop(nil) + end + for i = 0, 4 do + local tex = _G["BonusActionBarTexture" .. i] + if tex then tex:SetAlpha(0) end + end + end + + if MultiBarBottomLeftArtFrame then MultiBarBottomLeftArtFrame:Hide() end + if MultiBarBottomRightArtFrame then MultiBarBottomRightArtFrame:Hide() end + + -- 隐藏原版狮鹫端帽(Texture 对象,非 Frame) + local endcaps = { "MainMenuBarLeftEndCap", "MainMenuBarRightEndCap" } + for _, name in ipairs(endcaps) do + local f = _G[name] + if f then + f:Hide() + f.Show = function() end + end + end + + if MainMenuBarBackpackButton then MainMenuBarBackpackButton:Hide() end + for slot = 0, 3 do + local b = _G["CharacterBag" .. slot .. "Slot"] + if b then b:Hide() end + end + if KeyRingButton then KeyRingButton:Hide() end +end + +-------------------------------------------------------------------------------- +-- Create bar structure (once) +-------------------------------------------------------------------------------- +function AB:CreateBars() + local db = self:GetDB() + local size = db.buttonSize + local gap = db.buttonGap + local rowWidth = (size + gap) * BUTTONS_PER_ROW - gap + + -- === BOTTOM BARS === + local anchor = CreateFrame("Frame", "SFramesActionBarAnchor", UIParent) + anchor:SetWidth(rowWidth) + anchor:SetHeight(size * 3 + gap * 2) + anchor:SetPoint("BOTTOM", UIParent, "BOTTOM", db.bottomOffsetX, db.bottomOffsetY) + anchor:SetScale(db.scale) + self.anchor = anchor + + -- Row 1: ActionButton1-12 (safe to reparent, uses page calc) + local row1 = CreateFrame("Frame", "SFramesMainBar", anchor) + row1:SetWidth(rowWidth) + row1:SetHeight(size) + row1:SetPoint("BOTTOMLEFT", anchor, "BOTTOMLEFT", 0, 0) + self.row1 = row1 + + self.mainButtons = {} + for i = 1, BUTTONS_PER_ROW do + local b = _G["ActionButton" .. i] + if b then + b:SetParent(row1) + StyleButton(b) + table.insert(self.mainButtons, b) + end + end + + -- === BONUS ACTION BAR (druid forms, warrior stances, etc.) === + self.bonusButtons = {} + if BonusActionBarFrame then + BonusActionBarFrame:SetParent(row1) + BonusActionBarFrame:ClearAllPoints() + BonusActionBarFrame:SetPoint("BOTTOMLEFT", row1, "BOTTOMLEFT", 0, 0) + BonusActionBarFrame:SetWidth(rowWidth) + BonusActionBarFrame:SetHeight(size) + BonusActionBarFrame:SetFrameLevel(row1:GetFrameLevel() + 5) + + for i = 1, BUTTONS_PER_ROW do + local b = _G["BonusActionButton" .. i] + if b then + StyleButton(b) + table.insert(self.bonusButtons, b) + end + end + + BonusActionBarFrame:SetScript("OnShow", function() + for i = 1, BUTTONS_PER_ROW do + local b = _G["BonusActionButton" .. i] + if b and BonusActionButton_Update then + BonusActionButton_Update(b) + end + end + local db = AB:GetDB() + local s = db.buttonSize + local g = db.buttonGap + LayoutRow(AB.bonusButtons, BonusActionBarFrame, s, g) + local btnLevel = BonusActionBarFrame:GetFrameLevel() + 1 + for _, btn in ipairs(AB.bonusButtons) do + btn:EnableMouse(true) + btn:SetFrameLevel(btnLevel) + if btn.sfBackdrop then + btn.sfBackdrop:SetFrameLevel(btnLevel - 1) + end + end + end) + end + + -- === PAGE INDICATOR === + local pi = row1:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + pi:SetPoint("RIGHT", row1, "LEFT", -4, 0) + pi:SetTextColor(0.9, 0.8, 0.2, 0.9) + self.pageIndicator = pi + + -- Row 2: reposition the original MultiBarBottomLeft frame + if MultiBarBottomLeft then + MultiBarBottomLeft:SetParent(anchor) + MultiBarBottomLeft:ClearAllPoints() + MultiBarBottomLeft:SetPoint("BOTTOMLEFT", row1, "TOPLEFT", 0, gap) + MultiBarBottomLeft:SetWidth(rowWidth) + MultiBarBottomLeft:SetHeight(size) + MultiBarBottomLeft:Show() + end + self.row2 = MultiBarBottomLeft + + self.bar2Buttons = {} + for i = 1, BUTTONS_PER_ROW do + local b = _G["MultiBarBottomLeftButton" .. i] + if b then + b:Show() + StyleButton(b) + table.insert(self.bar2Buttons, b) + end + end + + -- Row 3: reposition the original MultiBarBottomRight frame + if MultiBarBottomRight then + MultiBarBottomRight:SetParent(anchor) + MultiBarBottomRight:ClearAllPoints() + MultiBarBottomRight:SetPoint("BOTTOMLEFT", MultiBarBottomLeft or row1, "TOPLEFT", 0, gap) + MultiBarBottomRight:SetWidth(rowWidth) + MultiBarBottomRight:SetHeight(size) + MultiBarBottomRight:Show() + end + self.row3 = MultiBarBottomRight + + self.bar3Buttons = {} + for i = 1, BUTTONS_PER_ROW do + local b = _G["MultiBarBottomRightButton" .. i] + if b then + b:Show() + StyleButton(b) + table.insert(self.bar3Buttons, b) + end + end + + -- === 狮鹫端帽 === + local srcL = MainMenuBarLeftEndCap + local capW = (srcL and srcL.GetWidth) and srcL:GetWidth() or 128 + local capH = (srcL and srcL.GetHeight) and srcL:GetHeight() or 76 + + local gryphonTex = GetGryphonTexPath(db.gryphonStyle) + + local leftCap = CreateFrame("Frame", "SFramesGryphonLeft", UIParent) + leftCap:SetWidth(capW) + leftCap:SetHeight(capH) + local leftImg = leftCap:CreateTexture(nil, "ARTWORK") + leftImg:SetAllPoints() + leftImg:SetTexture(gryphonTex) + leftCap._sfTex = leftImg + self.gryphonLeft = leftCap + + local rightCap = CreateFrame("Frame", "SFramesGryphonRight", UIParent) + rightCap:SetWidth(capW) + rightCap:SetHeight(capH) + local rightImg = rightCap:CreateTexture(nil, "ARTWORK") + rightImg:SetAllPoints() + rightImg:SetTexture(gryphonTex) + rightImg:SetTexCoord(1, 0, 0, 1) + rightCap._sfTex = rightImg + self.gryphonRight = rightCap + + + -- === RIGHT-SIDE BARS === + local rightHolder = CreateFrame("Frame", "SFramesRightBarHolder", UIParent) + rightHolder:SetWidth(size * 2 + gap) + rightHolder:SetHeight((size + gap) * BUTTONS_PER_ROW - gap) + rightHolder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY) + rightHolder:SetScale(db.scale) + self.rightHolder = rightHolder + + if MultiBarRight then + MultiBarRight:SetParent(rightHolder) + MultiBarRight:ClearAllPoints() + MultiBarRight:SetPoint("TOPRIGHT", rightHolder, "TOPRIGHT", 0, 0) + MultiBarRight:Show() + end + + self.rightButtons = {} + for i = 1, BUTTONS_PER_ROW do + local b = _G["MultiBarRightButton" .. i] + if b then + b:Show() + StyleButton(b) + table.insert(self.rightButtons, b) + end + end + + if MultiBarLeft then + MultiBarLeft:SetParent(rightHolder) + MultiBarLeft:ClearAllPoints() + MultiBarLeft:SetPoint("TOPRIGHT", MultiBarRight or rightHolder, "TOPLEFT", -gap, 0) + MultiBarLeft:Show() + end + + self.leftButtons = {} + for i = 1, BUTTONS_PER_ROW do + local b = _G["MultiBarLeftButton" .. i] + if b then + b:Show() + StyleButton(b) + table.insert(self.leftButtons, b) + end + end + + -- === STANCE BAR === + local stanceHolder = CreateFrame("Frame", "SFramesStanceHolder", UIParent) + stanceHolder:SetWidth(rowWidth) + stanceHolder:SetHeight(size) + stanceHolder:SetScale(db.scale) + self.stanceHolder = stanceHolder + + self.stanceButtons = {} + for i = 1, 10 do + local b = _G["ShapeshiftButton" .. i] + if b then + b:SetParent(stanceHolder) + StyleButton(b) + table.insert(self.stanceButtons, b) + end + end + + -- === PET BAR === + local petHolder = CreateFrame("Frame", "SFramesPetHolder", UIParent) + petHolder:SetWidth(rowWidth) + petHolder:SetHeight(size) + petHolder:SetScale(db.scale) + self.petHolder = petHolder + + self.petButtons = {} + for i = 1, 10 do + local b = _G["PetActionButton" .. i] + if b then + b:SetParent(petHolder) + StylePetButton(b) + table.insert(self.petButtons, b) + end + end + +end + +-------------------------------------------------------------------------------- +-- Adaptive text sizing: scale hotkey / count / macro-name fonts to button size +-------------------------------------------------------------------------------- +local function UpdateButtonTexts(buttons, btnSize, showHotkey, showMacroName) + local fontSize = math.max(6, math.floor(btnSize * 0.25 + 0.5)) + local nameSize = math.max(6, math.floor(btnSize * 0.22 + 0.5)) + local font = SFrames:GetFont() + for _, b in ipairs(buttons) do + local bname = b:GetName() + local hotkey = _G[bname .. "HotKey"] + if hotkey then + hotkey:SetFont(font, fontSize, "OUTLINE") + if showHotkey then hotkey:Show() else hotkey:Hide() end + end + local count = _G[bname .. "Count"] + if count then + count:SetFont(font, fontSize, "OUTLINE") + end + local mn = _G[bname .. "Name"] + if mn then + mn:SetFont(font, nameSize, "OUTLINE") + if showMacroName then mn:Show() else mn:Hide() end + end + end +end + +-------------------------------------------------------------------------------- +-- Apply config +-------------------------------------------------------------------------------- +function AB:ApplyConfig() + if not self.anchor then return end + local db = self:GetDB() + + local size = db.buttonSize + local gap = db.buttonGap + local rowWidth = (size + gap) * BUTTONS_PER_ROW - gap + local colHeight = (size + gap) * BUTTONS_PER_ROW - gap + local totalHeight = db.barCount * size + (db.barCount - 1) * gap + + -- Bottom bars anchor + self.anchor:SetScale(db.scale) + self.anchor:SetWidth(rowWidth) + self.anchor:SetHeight(totalHeight) + + -- Row 1 + self.row1:SetWidth(rowWidth) + self.row1:SetHeight(size) + LayoutRow(self.mainButtons, self.row1, size, gap) + + -- Bonus bar (druid forms) — same layout as row 1, overlays when active + if self.bonusButtons and BonusActionBarFrame then + BonusActionBarFrame:SetWidth(rowWidth) + BonusActionBarFrame:SetHeight(size) + LayoutRow(self.bonusButtons, BonusActionBarFrame, size, gap) + end + + -- Row 2 + if self.row2 then + self.row2:SetWidth(rowWidth) + self.row2:SetHeight(size) + self.row2:ClearAllPoints() + self.row2:SetPoint("BOTTOMLEFT", self.row1, "TOPLEFT", 0, gap) + LayoutRow(self.bar2Buttons, self.row2, size, gap) + if db.barCount >= 2 then self.row2:Show() else self.row2:Hide() end + end + + -- Row 3 + if self.row3 then + self.row3:SetWidth(rowWidth) + self.row3:SetHeight(size) + self.row3:ClearAllPoints() + self.row3:SetPoint("BOTTOMLEFT", self.row2 or self.row1, "TOPLEFT", 0, gap) + LayoutRow(self.bar3Buttons, self.row3, size, gap) + if db.barCount >= 3 then self.row3:Show() else self.row3:Hide() end + end + + -- Right-side bars + if self.rightHolder then + self.rightHolder:SetScale(db.scale) + self.rightHolder:SetWidth(size * 2 + gap) + self.rightHolder:SetHeight(colHeight) + + if MultiBarRight then + MultiBarRight:SetWidth(size) + MultiBarRight:SetHeight(colHeight) + MultiBarRight:ClearAllPoints() + MultiBarRight:SetPoint("TOPRIGHT", self.rightHolder, "TOPRIGHT", 0, 0) + LayoutColumn(self.rightButtons, MultiBarRight, size, gap) + end + + if MultiBarLeft then + MultiBarLeft:SetWidth(size) + MultiBarLeft:SetHeight(colHeight) + MultiBarLeft:ClearAllPoints() + MultiBarLeft:SetPoint("TOPRIGHT", MultiBarRight or self.rightHolder, "TOPLEFT", -gap, 0) + LayoutColumn(self.leftButtons, MultiBarLeft, size, gap) + end + + if db.showRightBars then + self.rightHolder:Show() + else + self.rightHolder:Hide() + end + end + + -- Hotkey / macro name(使用缓存表,避免每次 ApplyConfig 都分配临时表) + if not self.allButtonsCache then + self.allButtonsCache = {} + for _, b in ipairs(self.mainButtons) do table.insert(self.allButtonsCache, b) end + if self.bonusButtons then + for _, b in ipairs(self.bonusButtons) do table.insert(self.allButtonsCache, b) end + end + for _, b in ipairs(self.bar2Buttons) do table.insert(self.allButtonsCache, b) end + for _, b in ipairs(self.bar3Buttons) do table.insert(self.allButtonsCache, b) end + for _, b in ipairs(self.rightButtons) do table.insert(self.allButtonsCache, b) end + for _, b in ipairs(self.leftButtons) do table.insert(self.allButtonsCache, b) end + for _, b in ipairs(self.stanceButtons) do table.insert(self.allButtonsCache, b) end + for _, b in ipairs(self.petButtons) do table.insert(self.allButtonsCache, b) end + end + + -- Hotkey / macro name — per-group with adaptive font size + local showHK = db.showHotkey + local showMN = db.showMacroName + local smallSize = db.smallBarSize + UpdateButtonTexts(self.mainButtons, size, showHK, showMN) + if self.bonusButtons then + UpdateButtonTexts(self.bonusButtons, size, showHK, showMN) + end + UpdateButtonTexts(self.bar2Buttons, size, showHK, showMN) + UpdateButtonTexts(self.bar3Buttons, size, showHK, showMN) + UpdateButtonTexts(self.rightButtons, size, showHK, showMN) + UpdateButtonTexts(self.leftButtons, size, showHK, showMN) + UpdateButtonTexts(self.stanceButtons, smallSize, showHK, showMN) + UpdateButtonTexts(self.petButtons, smallSize, showHK, showMN) + + -- Button visual style (rounded corners, inner shadow) + local isRounded = db.buttonRounded + local isShadow = db.buttonInnerShadow + for _, b in ipairs(self.allButtonsCache) do + ApplyButtonVisuals(b, isRounded, isShadow) + end + + -- 始终显示动作条:强制 showgrid,让空格子也显示背景框 + self:ApplyAlwaysShowGrid() + + self:ApplyPosition() + self:ApplyStanceBar() + self:ApplyPetBar() + self:ApplyGryphon() + self:UpdateBonusBar() +end + +-------------------------------------------------------------------------------- +-- Always-show-grid: 对当前所有已显示行中的空格子,强制显示背景框和格子材质 +-- 不控制哪些行显示/隐藏,只控制空格子是否绘制背景 +-------------------------------------------------------------------------------- +function AB:ApplyAlwaysShowGrid() + local db = self:GetDB() + + -- 收集所有当前参与布局的按钮(已样式化的按钮) + local allBars = { + self.mainButtons, + self.bar2Buttons, + self.bar3Buttons, + self.rightButtons, + self.leftButtons, + } + + if db.alwaysShowGrid then + for _, bar in ipairs(allBars) do + if bar then + for _, b in ipairs(bar) do + if styledButtons[b] then + -- showgrid > 0 时 Blizzard 不会隐藏空格按钮 + b.showgrid = 1 + b:Show() + -- 背景子帧始终随按钮显示 + if b.sfBackdrop then b.sfBackdrop:Show() end + end + end + end + end + else + for _, bar in ipairs(allBars) do + if bar then + for _, b in ipairs(bar) do + if styledButtons[b] then + b.showgrid = 0 + -- 恢复 Blizzard 默认:无技能的格子隐藏 + local ok, action = pcall(ActionButton_GetPagedID, b) + if ok and action and not HasAction(action) then + b:Hide() + end + end + end + end + end + end + + if BonusActionBarFrame and BonusActionBarFrame:IsShown() then + for _, b in ipairs(self.mainButtons) do + b:Hide() + end + end +end + +-------------------------------------------------------------------------------- +-- Stance bar +-------------------------------------------------------------------------------- +function AB:ApplyStanceBar() + local db = self:GetDB() + local size = db.smallBarSize + local gap = db.buttonGap + + local numForms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0 + + if not db.showStanceBar or numForms == 0 then + if self.stanceHolder then self.stanceHolder:Hide() end + return + end + + self.stanceHolder:SetScale(db.scale) + local topRow = self.row1 + if db.barCount >= 3 and self.row3 then topRow = self.row3 + elseif db.barCount >= 2 and self.row2 then topRow = self.row2 end + + self.stanceHolder:ClearAllPoints() + self.stanceHolder:SetPoint("BOTTOMLEFT", topRow, "TOPLEFT", 0, gap) + + local totalW = numForms * size + (numForms - 1) * gap + self.stanceHolder:SetWidth(totalW) + self.stanceHolder:SetHeight(size) + self.stanceHolder:Show() + + for i, b in ipairs(self.stanceButtons) do + if i <= numForms then + b:SetWidth(size) + b:SetHeight(size) + b:ClearAllPoints() + if i == 1 then + b:SetPoint("BOTTOMLEFT", self.stanceHolder, "BOTTOMLEFT", 0, 0) + else + b:SetPoint("LEFT", self.stanceButtons[i - 1], "RIGHT", gap, 0) + end + b:Show() + else + b:Hide() + end + end +end + +-------------------------------------------------------------------------------- +-- Pet bar +-------------------------------------------------------------------------------- +function AB:ApplyPetBar() + local db = self:GetDB() + local size = db.smallBarSize + local gap = db.buttonGap + + local hasPet = HasPetUI and HasPetUI() + if not db.showPetBar or not hasPet then + if self.petHolder then self.petHolder:Hide() end + return + end + + self.petHolder:SetScale(db.scale) + + self.petHolder:ClearAllPoints() + local numForms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0 + if db.showStanceBar and numForms > 0 and self.stanceHolder:IsShown() then + self.petHolder:SetPoint("BOTTOMLEFT", self.stanceHolder, "TOPLEFT", 0, gap) + else + local topRow = self.row1 + if db.barCount >= 3 and self.row3 then topRow = self.row3 + elseif db.barCount >= 2 and self.row2 then topRow = self.row2 end + self.petHolder:SetPoint("BOTTOMLEFT", topRow, "TOPLEFT", 0, gap) + end + + local numPet = 10 + local totalW = numPet * size + (numPet - 1) * gap + self.petHolder:SetWidth(totalW) + self.petHolder:SetHeight(size) + self.petHolder:Show() + + for i, b in ipairs(self.petButtons) do + b:SetWidth(size) + b:SetHeight(size) + b:ClearAllPoints() + if i == 1 then + b:SetPoint("BOTTOMLEFT", self.petHolder, "BOTTOMLEFT", 0, 0) + else + b:SetPoint("LEFT", self.petButtons[i - 1], "RIGHT", gap, 0) + end + b:Show() + end +end + +-------------------------------------------------------------------------------- +-- Gryphon end-caps +-------------------------------------------------------------------------------- +function AB:ApplyGryphon() + if not self.gryphonLeft or not self.gryphonRight then return end + if not self.anchor then return end + + local db = self:GetDB() + + if db.hideGryphon then + self.gryphonLeft:Hide() + self.gryphonRight:Hide() + return + end + + -- 切换纹理样式 + local texPath = GetGryphonTexPath(db.gryphonStyle) + if self.gryphonLeft._sfTex then + self.gryphonLeft._sfTex:SetTexture(texPath) + end + if self.gryphonRight._sfTex then + self.gryphonRight._sfTex:SetTexture(texPath) + self.gryphonRight._sfTex:SetTexCoord(1, 0, 0, 1) + end + + local scale = db.scale or 1.0 + local gw = db.gryphonWidth or 64 + local gh = db.gryphonHeight or 64 + + self.gryphonLeft:SetScale(scale) + self.gryphonRight:SetScale(scale) + self.gryphonLeft:SetWidth(gw) + self.gryphonLeft:SetHeight(gh) + self.gryphonRight:SetWidth(gw) + self.gryphonRight:SetHeight(gh) + + if db.gryphonOnTop then + self.gryphonLeft:SetFrameLevel(self.anchor:GetFrameLevel() + 10) + self.gryphonRight:SetFrameLevel(self.anchor:GetFrameLevel() + 10) + else + self.gryphonLeft:SetFrameLevel(0) + self.gryphonRight:SetFrameLevel(0) + end + + local ox = db.gryphonOffsetX or 30 + local oy = db.gryphonOffsetY or 0 + + self.gryphonLeft:ClearAllPoints() + self.gryphonLeft:SetPoint("BOTTOMRIGHT", self.anchor, "BOTTOMLEFT", ox, oy) + + self.gryphonRight:ClearAllPoints() + self.gryphonRight:SetPoint("BOTTOMLEFT", self.anchor, "BOTTOMRIGHT", -ox, oy) + + self.gryphonLeft:Show() + self.gryphonRight:Show() +end + +-------------------------------------------------------------------------------- +-- Slider-based position update +-------------------------------------------------------------------------------- +function AB:ApplyPosition() + local db = self:GetDB() + if self.anchor then + self.anchor:ClearAllPoints() + self.anchor:SetPoint("BOTTOM", UIParent, "BOTTOM", db.bottomOffsetX, db.bottomOffsetY) + end + if self.rightHolder then + self.rightHolder:ClearAllPoints() + self.rightHolder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY) + end +end + +-------------------------------------------------------------------------------- +-- Page indicator: shows which page / bonus offset the main bar is displaying +-------------------------------------------------------------------------------- +function AB:UpdatePageIndicator() + if not self.pageIndicator then return end + local page = CURRENT_ACTIONBAR_PAGE or 1 + local offset = GetBonusBarOffset and GetBonusBarOffset() or 0 + if offset > 0 and page == 1 then + self.pageIndicator:SetTextColor(1.0, 0.5, 0.0) + self.pageIndicator:SetText("B" .. offset) + else + self.pageIndicator:SetTextColor(0.9, 0.8, 0.2) + self.pageIndicator:SetText(tostring(page)) + end +end + +-------------------------------------------------------------------------------- +-- Bonus bar show/hide (warriors, druids, rogues — any class with stance/form) +-- Blizzard's ShowBonusActionBar() is triggered in MainMenuBar's OnEvent, but we +-- unregistered MainMenuBar events, so we must manage this ourselves. +-------------------------------------------------------------------------------- +function AB:UpdateBonusBar() + if not BonusActionBarFrame then return end + local offset = GetBonusBarOffset and GetBonusBarOffset() or 0 + local page = CURRENT_ACTIONBAR_PAGE or 1 + if page == 1 and offset > 0 then + for _, b in ipairs(self.mainButtons) do b:Hide() end + BonusActionBarFrame:Show() + for i = 0, 4 do + local tex = _G["BonusActionBarTexture" .. i] + if tex then tex:SetAlpha(0) end + end + local db = self:GetDB() + local size = db.buttonSize + local gap = db.buttonGap + local rowWidth = (size + gap) * BUTTONS_PER_ROW - gap + BonusActionBarFrame:ClearAllPoints() + BonusActionBarFrame:SetPoint("BOTTOMLEFT", self.row1, "BOTTOMLEFT", 0, 0) + BonusActionBarFrame:SetWidth(rowWidth) + BonusActionBarFrame:SetHeight(size) + BonusActionBarFrame:SetFrameLevel(self.row1:GetFrameLevel() + 5) + LayoutRow(self.bonusButtons, BonusActionBarFrame, size, gap) + local btnLevel = BonusActionBarFrame:GetFrameLevel() + 1 + for _, b in ipairs(self.bonusButtons) do + b:EnableMouse(true) + b:SetFrameLevel(btnLevel) + if b.sfBackdrop then + b.sfBackdrop:SetFrameLevel(btnLevel - 1) + end + end + else + BonusActionBarFrame:Hide() + for _, b in ipairs(self.mainButtons) do b:Show() end + end + self:UpdatePageIndicator() +end + +-------------------------------------------------------------------------------- +-- Range coloring - uses a separate red overlay; NEVER touches icon VertexColor +-------------------------------------------------------------------------------- +local function GetOrCreateRangeOverlay(b) + if b.sfRangeOverlay then return b.sfRangeOverlay end + local inset = b.sfIconInset or 2 + local ov = b:CreateTexture(nil, "OVERLAY") + ov:SetTexture("Interface\\Buttons\\WHITE8X8") + ov:SetPoint("TOPLEFT", b, "TOPLEFT", inset, -inset) + ov:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -inset, inset) + ov:SetVertexColor(1.0, 0.1, 0.1, 0.35) + ov:Hide() + b.sfRangeOverlay = ov + return ov +end + +function AB:SetupRangeCheck() + local rangeFrame = CreateFrame("Frame", "SFramesActionBarRangeCheck", UIParent) + rangeFrame.timer = 0 + self.rangeFrame = rangeFrame + + rangeFrame:SetScript("OnUpdate", function() + this.timer = this.timer + arg1 + if this.timer < 0.2 then return end + this.timer = 0 + + local db = AB:GetDB() + if not db.rangeColoring then return end + + local function CheckRange(buttons, idFunc) + local getID = idFunc or ActionButton_GetPagedID + if not getID then return end + for _, b in ipairs(buttons) do + local ok, action = pcall(getID, b) + if ok and action and HasAction(action) then + local inRange = IsActionInRange(action) + local ov = GetOrCreateRangeOverlay(b) + if inRange == 0 then + ov:Show() + else + ov:Hide() + end + else + if b.sfRangeOverlay then b.sfRangeOverlay:Hide() end + end + end + end + + CheckRange(AB.mainButtons) + if AB.bonusButtons and BonusActionBarFrame and BonusActionBarFrame:IsShown() then + CheckRange(AB.bonusButtons, BonusActionButton_GetPagedID) + end + CheckRange(AB.bar2Buttons) + CheckRange(AB.bar3Buttons) + end) +end + + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function AB:Initialize() + local db = self:GetDB() + if not db.enable then return end + + self:HideBlizzardBars() + self:CreateBars() + self:ApplyConfig() + self:SetupRangeCheck() + + -- Hook Blizzard ActionButton functions that reset NormalTexture. + -- These use 'this' (no explicit args in vanilla), so parameterless hook is safe. + local function SuppressNormalTexture() + if this and styledButtons[this] then + local nt = _G[this:GetName() .. "NormalTexture"] + HideNormalTexture(nt) + end + end + -- alwaysShowGrid 模式下,阻止空格子被隐藏 + local function ForceShowIfGrid() + if this and styledButtons[this] then + local adb = AB:GetDB() + if adb.alwaysShowGrid then + this.showgrid = 1 + this:Show() + if this.sfBackdrop then this.sfBackdrop:Show() end + end + end + end + local hookTargets = { + "ActionButton_Update", + "ActionButton_UpdateUsable", + "ActionButton_UpdateState", + } + for _, fn in ipairs(hookTargets) do + local orig = _G[fn] + if orig then + _G[fn] = function() + pcall(orig) + SuppressNormalTexture() + ForceShowIfGrid() + end + end + end + -- Hook ActionButton_ShowGrid / ActionButton_HideGrid + local origShowGrid = _G["ActionButton_ShowGrid"] + local origHideGrid = _G["ActionButton_HideGrid"] + + -- 判断某个按钮是否属于主动作条(ActionButton1-12) + local function IsMainButton(b) + if not b or not AB.mainButtons then return false end + for _, mb in ipairs(AB.mainButtons) do + if mb == b then return true end + end + return false + end + + -- 当前是否处于 BonusBar 激活状态(潜行、变身等) + local function IsBonusBarActive() + local offset = GetBonusBarOffset and GetBonusBarOffset() or 0 + local page = CURRENT_ACTIONBAR_PAGE or 1 + return (page == 1 and offset > 0) + end + + if origShowGrid then + _G["ActionButton_ShowGrid"] = function() + if this and this.showgrid == nil then this.showgrid = 0 end + if this and styledButtons[this] and IsMainButton(this) and IsBonusBarActive() then + SuppressNormalTexture() + return + end + pcall(origShowGrid) + SuppressNormalTexture() + end + end + if origHideGrid then + _G["ActionButton_HideGrid"] = function() + if this and this.showgrid == nil then this.showgrid = 0 end + if this and styledButtons[this] then + local adb = AB:GetDB() + if IsMainButton(this) and IsBonusBarActive() then + this.showgrid = 0 + this:Hide() + SuppressNormalTexture() + return + end + if adb.alwaysShowGrid then + this.showgrid = 1 + this:Show() + SuppressNormalTexture() + return + end + end + pcall(origHideGrid) + SuppressNormalTexture() + end + end + + -- BonusActionButton functions take an explicit button arg — must pass it through + local function SuppressNT(b) + if b and styledButtons[b] then + local nt = _G[b:GetName() .. "NormalTexture"] + HideNormalTexture(nt) + end + end + local origBonusUpdate = _G["BonusActionButton_Update"] + if origBonusUpdate then + _G["BonusActionButton_Update"] = function(button) + pcall(origBonusUpdate, button) + local b = button or this + SuppressNT(b) + if b and styledButtons[b] and AB.bonusButtons then + local db = AB:GetDB() + local size = db.buttonSize + local gap = db.buttonGap + for idx, btn in ipairs(AB.bonusButtons) do + if btn == b then + btn:SetWidth(size) + btn:SetHeight(size) + btn:ClearAllPoints() + if idx == 1 then + btn:SetPoint("BOTTOMLEFT", BonusActionBarFrame, "BOTTOMLEFT", 0, 0) + else + btn:SetPoint("LEFT", AB.bonusButtons[idx - 1], "RIGHT", gap, 0) + end + break + end + end + end + end + end + local origBonusUsable = _G["BonusActionButton_UpdateUsable"] + if origBonusUsable then + _G["BonusActionButton_UpdateUsable"] = function(button) + pcall(origBonusUsable, button) + SuppressNT(button or this) + end + end + + -- Hook PetActionBar_Update: Blizzard resets NormalTexture2 vertex color/alpha + local origPetBarUpdate = PetActionBar_Update + if origPetBarUpdate then + PetActionBar_Update = function() + pcall(origPetBarUpdate) + for _, b in ipairs(AB.petButtons or {}) do + KillPetNormalTextures(b) + end + end + end + + -- Hook MultiActionBar_Update: Blizzard 在 PLAYER_ENTERING_WORLD 等事件中调用此函数, + -- 它会根据经验条/声望条的高度重设 MultiBarBottomLeft/Right 的位置,覆盖我们的布局。 + -- 仅修正 row2/row3 的锚点,不调用 ApplyConfig 避免触发 MultiBarBottomLeft:Show 再次递归。 + local origMultiActionBarUpdate = MultiActionBar_Update + if origMultiActionBarUpdate then + local inMultiBarHook = false + MultiActionBar_Update = function() + pcall(origMultiActionBarUpdate) + if AB.anchor and not inMultiBarHook then + inMultiBarHook = true + local g = AB:GetDB().buttonGap + if AB.row2 then + AB.row2:ClearAllPoints() + AB.row2:SetPoint("BOTTOMLEFT", AB.row1, "TOPLEFT", 0, g) + end + if AB.row3 then + AB.row3:ClearAllPoints() + AB.row3:SetPoint("BOTTOMLEFT", AB.row2 or AB.row1, "TOPLEFT", 0, g) + end + inMultiBarHook = false + end + end + end + + -- Hook UIParent_ManageFramePositions: Blizzard 在进出战斗、切换地图等场景调用此 + -- 函数重排 MultiBar 位置(会考虑经验条/声望条高度),覆盖我们的布局。 + -- 在原函数执行完毕后立即修正 row2/row3 锚点。 + local origManageFramePositions = UIParent_ManageFramePositions + if origManageFramePositions then + UIParent_ManageFramePositions = function(a1, a2, a3) + origManageFramePositions(a1, a2, a3) + if AB.anchor and AB.row1 then + local g = AB:GetDB().buttonGap + if AB.row2 then + AB.row2:ClearAllPoints() + AB.row2:SetPoint("BOTTOMLEFT", AB.row1, "TOPLEFT", 0, g) + end + if AB.row3 then + AB.row3:ClearAllPoints() + AB.row3:SetPoint("BOTTOMLEFT", AB.row2 or AB.row1, "TOPLEFT", 0, g) + end + end + if SFrames and SFrames.Chat then + SFrames.Chat:ReanchorChatFrames() + end + end + end + + -- Direct event-driven bonus bar update + safety poller. + -- Events trigger immediate update; poller catches any missed state every 0.2s. + SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() + AB:UpdateBonusBar() + AB:ApplyConfig() + AB:RefreshAllHotkeys() + end) + + SFrames:RegisterEvent("UPDATE_BINDINGS", function() + AB:RefreshAllHotkeys() + end) + + SFrames:RegisterEvent("UPDATE_BONUS_ACTIONBAR", function() + AB:UpdateBonusBar() + end) + + SFrames:RegisterEvent("ACTIONBAR_PAGE_CHANGED", function() + AB:UpdateBonusBar() + end) + + SFrames:RegisterEvent("UPDATE_SHAPESHIFT_FORMS", function() + AB:UpdateBonusBar() + AB:ApplyStanceBar() + AB:ApplyPetBar() + end) + + -- Safety poller: if Blizzard code or timing issues cause a mismatch, + -- correct it within 0.2 seconds. + local bonusPoller = CreateFrame("Frame") + bonusPoller.timer = 0 + bonusPoller:SetScript("OnUpdate", function() + this.timer = this.timer + arg1 + if this.timer < 0.2 then return end + this.timer = 0 + local offset = GetBonusBarOffset and GetBonusBarOffset() or 0 + local page = CURRENT_ACTIONBAR_PAGE or 1 + local want = (page == 1 and offset > 0) + local have = BonusActionBarFrame and BonusActionBarFrame:IsShown() + if (want and not have) or (not want and have) then + AB:UpdateBonusBar() + elseif have then + local db = AB:GetDB() + local size = db.buttonSize + local gap = db.buttonGap + local rowWidth = (size + gap) * BUTTONS_PER_ROW - gap + BonusActionBarFrame:ClearAllPoints() + BonusActionBarFrame:SetPoint("BOTTOMLEFT", AB.row1, "BOTTOMLEFT", 0, 0) + BonusActionBarFrame:SetWidth(rowWidth) + BonusActionBarFrame:SetHeight(size) + BonusActionBarFrame:SetFrameLevel(AB.row1:GetFrameLevel() + 5) + LayoutRow(AB.bonusButtons, BonusActionBarFrame, size, gap) + local btnLevel = BonusActionBarFrame:GetFrameLevel() + 1 + for _, b in ipairs(AB.bonusButtons) do + b:EnableMouse(true) + b:SetFrameLevel(btnLevel) + if b.sfBackdrop then + b.sfBackdrop:SetFrameLevel(btnLevel - 1) + end + end + for _, b in ipairs(AB.mainButtons) do + if b:IsShown() then b:Hide() end + end + end + AB:UpdatePageIndicator() + end) + + SFrames:RegisterEvent("PET_BAR_UPDATE", function() + AB:ApplyPetBar() + end) + + SFrames:RegisterEvent("UNIT_PET", function() + if arg1 == "player" then + AB:ApplyPetBar() + end + end) + + -- 进出战斗和切换区域时立即修正行间距,防止 Blizzard 布局覆盖 + local function FixRowAnchors() + if not AB.anchor or not AB.row1 then return end + local g = AB:GetDB().buttonGap + if AB.row2 then + AB.row2:ClearAllPoints() + AB.row2:SetPoint("BOTTOMLEFT", AB.row1, "TOPLEFT", 0, g) + end + if AB.row3 then + AB.row3:ClearAllPoints() + AB.row3:SetPoint("BOTTOMLEFT", AB.row2 or AB.row1, "TOPLEFT", 0, g) + end + end + + SFrames:RegisterEvent("PLAYER_REGEN_DISABLED", FixRowAnchors) + SFrames:RegisterEvent("PLAYER_REGEN_ENABLED", FixRowAnchors) + SFrames:RegisterEvent("ZONE_CHANGED", FixRowAnchors) + SFrames:RegisterEvent("ZONE_CHANGED_NEW_AREA", FixRowAnchors) + SFrames:RegisterEvent("ZONE_CHANGED_INDOORS", FixRowAnchors) +end + +-------------------------------------------------------------------------------- +-- Key Binding Mode +-- Hover any action/stance/pet button and press a key to bind it. +-- ESC exits. Right-click clears. Mouse wheel supported. +-------------------------------------------------------------------------------- +local BUTTON_BINDING_MAP = {} + +do + for i = 1, 12 do + BUTTON_BINDING_MAP["ActionButton" .. i] = "ACTIONBUTTON" .. i + BUTTON_BINDING_MAP["MultiBarBottomLeftButton" .. i] = "MULTIACTIONBAR1BUTTON" .. i + BUTTON_BINDING_MAP["MultiBarBottomRightButton" .. i] = "MULTIACTIONBAR2BUTTON" .. i + BUTTON_BINDING_MAP["MultiBarRightButton" .. i] = "MULTIACTIONBAR3BUTTON" .. i + BUTTON_BINDING_MAP["MultiBarLeftButton" .. i] = "MULTIACTIONBAR4BUTTON" .. i + end + for i = 1, 10 do + BUTTON_BINDING_MAP["ShapeshiftButton" .. i] = "SHAPESHIFTBUTTON" .. i + BUTTON_BINDING_MAP["PetActionButton" .. i] = "BONUSACTIONBUTTON" .. i + end +end + +-------------------------------------------------------------------------------- +-- Hotkey text refresh: update the HotKey FontString on buttons to reflect +-- current keybindings (works for all bars including stance and pet). +-------------------------------------------------------------------------------- +local function RefreshButtonHotkey(button) + if not button then return end + local name = button:GetName() + if not name then return end + local cmd = BUTTON_BINDING_MAP[name] + if not cmd then return end + local hotkey = _G[name .. "HotKey"] + if not hotkey then return end + local key1 = GetBindingKey(cmd) + if key1 then + local text = key1 + if GetBindingText then + text = GetBindingText(key1, "KEY_", 1) or key1 + end + hotkey:SetText(text) + else + hotkey:SetText("") + end +end + +function AB:RefreshAllHotkeys() + local function Refresh(list) + if not list then return end + for _, b in ipairs(list) do RefreshButtonHotkey(b) end + end + Refresh(self.mainButtons) + Refresh(self.bonusButtons) + Refresh(self.bar2Buttons) + Refresh(self.bar3Buttons) + Refresh(self.rightButtons) + Refresh(self.leftButtons) + Refresh(self.stanceButtons) + Refresh(self.petButtons) +end + +local IGNORE_KEYS = { + LSHIFT = true, RSHIFT = true, + LCTRL = true, RCTRL = true, + LALT = true, RALT = true, + UNKNOWN = true, +} + +local function BuildKeyString(key) + if IGNORE_KEYS[key] then return nil end + local mods = "" + if IsAltKeyDown() then mods = mods .. "ALT-" end + if IsControlKeyDown() then mods = mods .. "CTRL-" end + if IsShiftKeyDown() then mods = mods .. "SHIFT-" end + return mods .. key +end + +local function GetButtonBindingCmd(button) + if not button then return nil end + local name = button:GetName() + return name and BUTTON_BINDING_MAP[name] +end + +local function ShortKeyText(key) + if not key then return "" end + key = string.gsub(key, "SHIFT%-", "S-") + key = string.gsub(key, "CTRL%-", "C-") + key = string.gsub(key, "ALT%-", "A-") + key = string.gsub(key, "MOUSEWHEELUP", "WhlUp") + key = string.gsub(key, "MOUSEWHEELDOWN", "WhlDn") + return key +end + +local keyBindActive = false +local hoveredBindButton = nil +local keyBindOverlays = {} +local captureFrame = nil +local statusFrame = nil + +local function UpdateOverlayText(button) + local ov = keyBindOverlays[button] + if not ov then return end + local cmd = GetButtonBindingCmd(button) + if not cmd then ov.label:SetText(""); return end + local key1, key2 = GetBindingKey(cmd) + local t = "" + if key1 then t = ShortKeyText(key1) end + if key2 then t = t .. "\n" .. ShortKeyText(key2) end + ov.label:SetText(t) +end + +local function CreateBindOverlay(button) + if keyBindOverlays[button] then return keyBindOverlays[button] end + + local ov = CreateFrame("Button", nil, button) + ov:SetAllPoints(button) + ov:SetFrameLevel(button:GetFrameLevel() + 10) + ov:RegisterForClicks("RightButtonUp") + + local bg = ov:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints() + bg:SetTexture(0, 0, 0, 0.55) + + local label = ov:CreateFontString(nil, "OVERLAY") + label:SetFont(SFrames:GetFont(), 9, "OUTLINE") + label:SetPoint("CENTER", ov, "CENTER", 0, 0) + label:SetTextColor(1, 0.82, 0) + ov.label = label + + local highlight = ov:CreateTexture(nil, "HIGHLIGHT") + highlight:SetAllPoints() + highlight:SetTexture(1, 1, 1, 0.18) + + ov:SetScript("OnEnter", function() + hoveredBindButton = button + GameTooltip:SetOwner(this, "ANCHOR_TOP") + local cmd = GetButtonBindingCmd(button) + if cmd then + GameTooltip:AddLine(cmd, 1, 0.82, 0) + local k1, k2 = GetBindingKey(cmd) + if k1 then GameTooltip:AddLine("Key 1: " .. k1, 1, 1, 1) end + if k2 then GameTooltip:AddLine("Key 2: " .. k2, 0.7, 0.7, 0.7) end + end + GameTooltip:AddLine("Press key to bind / Wheel to bind", 0.5, 0.8, 0.5) + GameTooltip:AddLine("Right-click to clear", 0.8, 0.5, 0.5) + GameTooltip:Show() + end) + ov:SetScript("OnLeave", function() + hoveredBindButton = nil + GameTooltip:Hide() + end) + ov:SetScript("OnClick", function() + if arg1 == "RightButton" then + local cmd = GetButtonBindingCmd(button) + if cmd then + local k1, k2 = GetBindingKey(cmd) + if k2 then SetBinding(k2, nil) end + if k1 then SetBinding(k1, nil) end + SaveBindings(2) + UpdateOverlayText(button) + RefreshButtonHotkey(button) + DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami-UI]|r Cleared bindings for " .. cmd) + end + end + end) + + ov:EnableMouseWheel(true) + ov:SetScript("OnMouseWheel", function() + local cmd = GetButtonBindingCmd(button) + if not cmd then return end + local wheel = (arg1 > 0) and "MOUSEWHEELUP" or "MOUSEWHEELDOWN" + local mods = "" + if IsAltKeyDown() then mods = mods .. "ALT-" end + if IsControlKeyDown() then mods = mods .. "CTRL-" end + if IsShiftKeyDown() then mods = mods .. "SHIFT-" end + local keyStr = mods .. wheel + local old = GetBindingAction(keyStr) + if old and old ~= "" and old ~= cmd then SetBinding(keyStr, nil) end + SetBinding(keyStr, cmd) + SaveBindings(2) + UpdateOverlayText(button) + RefreshButtonHotkey(button) + DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami-UI]|r " .. keyStr .. " -> " .. cmd) + end) + + ov:Hide() + keyBindOverlays[button] = ov + return ov +end + +function AB:EnterKeyBindMode() + if keyBindActive then return end + keyBindActive = true + + local allButtons = {} + local function Collect(list) + if not list then return end + for _, b in ipairs(list) do table.insert(allButtons, b) end + end + Collect(self.mainButtons) + Collect(self.bar2Buttons) + Collect(self.bar3Buttons) + Collect(self.rightButtons) + Collect(self.leftButtons) + Collect(self.stanceButtons) + Collect(self.petButtons) + + for _, b in ipairs(allButtons) do + local ov = CreateBindOverlay(b) + ov:Show() + UpdateOverlayText(b) + end + + if not captureFrame then + captureFrame = CreateFrame("Frame", "SFramesKeyBindCapture", UIParent) + captureFrame:SetFrameStrata("TOOLTIP") + captureFrame:SetWidth(1) + captureFrame:SetHeight(1) + captureFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 0, 0) + captureFrame:EnableKeyboard(true) + captureFrame:EnableMouse(false) + captureFrame:SetScript("OnKeyDown", function() + local key = arg1 + if key == "ESCAPE" then + AB:ExitKeyBindMode() + return + end + if not hoveredBindButton then return end + local keyStr = BuildKeyString(key) + if not keyStr then return end + local cmd = GetButtonBindingCmd(hoveredBindButton) + if not cmd then return end + local old = GetBindingAction(keyStr) + if old and old ~= "" and old ~= cmd then SetBinding(keyStr, nil) end + SetBinding(keyStr, cmd) + SaveBindings(2) + UpdateOverlayText(hoveredBindButton) + RefreshButtonHotkey(hoveredBindButton) + DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami-UI]|r " .. keyStr .. " -> " .. cmd) + end) + end + captureFrame:Show() + + if not statusFrame then + statusFrame = CreateFrame("Frame", "SFramesKeyBindStatus", UIParent) + statusFrame:SetFrameStrata("TOOLTIP") + statusFrame:SetWidth(340) + statusFrame:SetHeight(100) + statusFrame:SetPoint("TOP", UIParent, "TOP", 0, -40) + statusFrame:SetMovable(true) + statusFrame:EnableMouse(true) + statusFrame:RegisterForDrag("LeftButton") + statusFrame:SetScript("OnDragStart", function() this:StartMoving() end) + statusFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + SFrames:CreateBackdrop(statusFrame) + + local title = statusFrame:CreateFontString(nil, "OVERLAY") + title:SetFont(SFrames:GetFont(), 13, "OUTLINE") + title:SetPoint("TOP", statusFrame, "TOP", 0, -12) + title:SetText("|cffffcc00按键绑定模式|r") + + local desc = statusFrame:CreateFontString(nil, "OVERLAY") + desc:SetFont(SFrames:GetFont(), 10, "OUTLINE") + desc:SetPoint("TOP", title, "BOTTOM", 0, -6) + desc:SetWidth(300) + desc:SetJustifyH("CENTER") + desc:SetText("悬停按钮 + 按键绑定 | 右键清除 | 滚轮绑定") + desc:SetTextColor(0.82, 0.82, 0.82) + + local saveBtn = CreateFrame("Button", "SFramesKeyBindSave", statusFrame, "UIPanelButtonTemplate") + saveBtn:SetWidth(120) + saveBtn:SetHeight(26) + saveBtn:SetPoint("BOTTOM", statusFrame, "BOTTOM", 0, 10) + saveBtn:SetText("保存并退出") + saveBtn:SetScript("OnClick", function() + AB:ExitKeyBindMode() + end) + local fs = saveBtn:GetFontString() + if fs then fs:SetFont(SFrames:GetFont(), 11) end + end + statusFrame:Show() + + DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami-UI]|r 按键绑定模式已开启。悬停按钮后按键绑定,右键清除,点击保存退出。") +end + +function AB:ExitKeyBindMode() + if not keyBindActive then return end + keyBindActive = false + hoveredBindButton = nil + + for _, ov in pairs(keyBindOverlays) do + ov:Hide() + end + if captureFrame then captureFrame:Hide() end + if statusFrame then statusFrame:Hide() end + + self:RefreshAllHotkeys() + DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami-UI]|r 按键绑定已保存。") +end + +function AB:ToggleKeyBindMode() + if keyBindActive then + self:ExitKeyBindMode() + else + self:EnterKeyBindMode() + end +end + +function AB:IsKeyBindMode() + return keyBindActive +end diff --git a/Bags/Bank.lua b/Bags/Bank.lua new file mode 100644 index 0000000..2eab792 --- /dev/null +++ b/Bags/Bank.lua @@ -0,0 +1,2087 @@ +-------------------------------------------------------------------------------- +-- S-Frames: Bank Module GUI (Bags/Bank.lua) +-- Unified custom interface for the player's Bank and extended Bank Bags +-------------------------------------------------------------------------------- + +SFrames.Bags.Bank = {} +local SFBankFrame = nil +local ItemSlots = {} +local isClosing = false -- Guard to prevent close闂傚倸鍊烽悞锕傚礈濮樿泛纾婚柛娑卞枟閸欏繘鏌嶈閹叉矠nt闂傚倸鍊烽悞锕傚礈濮樿泛纾婚柛娑卞枙缁诲棛绱掑顔界厪se recursion + +local SLOT_SIZE = 34 +local SPACING = 6 +local MARGIN = 10 +local TOP_OFFSET = 52 -- Space for title + search bar row +local BOTTOM_OFFSET = 34 -- Space for bank bag slot controls +local TEXT_EMPTY = "\231\169\186" +local TEXT_ITEM = "\231\137\169\229\147\129" +local TEXT_BANK_TITLE = "\233\147\182\232\161\140" +local TEXT_SORT = "\230\149\180\231\144\134" +local TEXT_BUY_SLOT = "\232\180\173\228\185\176\230\160\143\228\189\141" +local TEXT_UNAVAILABLE_OFFLINE = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\228\184\141\229\143\175\231\148\168" +local TEXT_BANK_BAG_SLOT = "\233\147\182\232\161\140\232\131\140\229\140\133\230\167\189" +local TEXT_BANK_BAG = "\233\147\182\232\161\140\232\131\140\229\140\133" +local TEXT_LOCKED_BANK_SLOT = "\229\183\178\233\148\129\229\174\154\231\154\132\233\147\182\232\161\140\232\131\140\229\140\133\230\167\189" +local TEXT_CLICK_BUY = "\231\130\185\229\135\187\232\180\173\228\185\176\232\175\165\230\160\143\228\189\141" +local TEXT_BUY_PREV_FIRST = "\232\175\183\229\133\136\232\180\173\228\185\176\229\137\141\228\184\128\228\184\170\230\160\143\228\189\141" +local TEXT_DRAG_EQUIP = "\230\139\150\229\133\165\232\131\140\229\140\133\229\143\175\232\163\133\229\164\135\229\136\176\230\173\164\230\160\143\228\189\141" +local TEXT_RIGHT_PICKUP = "\229\143\179\233\148\174\229\143\150\228\184\139\229\183\178\232\163\133\229\164\135\232\131\140\229\140\133" +local TEXT_CHARACTER = "\232\167\146\232\137\178" +local TEXT_ONLINE = "\229\156\168\231\186\191" +local TEXT_OFFLINE = "\231\166\187\231\186\191" +local TEXT_LAYOUT_ERR = "\233\147\182\232\161\140\229\184\131\229\177\128\230\155\180\230\150\176\229\164\177\232\180\165\239\188\154" +local TEXT_SLOT_UNIT = "\230\160\188" +local DEFAULT_BAG_ICON = "Interface\\Buttons\\Button-Backpack-Up" +local EMPTY_BAG_ICON = "Interface\\Icons\\INV_Misc_Bag_08" +local CHARACTER_SELECTOR_ICON = "Interface\\CHARACTERFRAME\\TemporaryPortrait-Female-Human" +local PANEL_BG_ALPHA = 0.55 +local SLOT_BG_ALPHA = 0.22 + +local _A = SFrames.ActiveTheme + +local bankSearchText = "" -- Current search filter text +local BankBagButtons = {} +local bankLayoutErrorStamp = nil + +local BANK_BAG_SIZE = 22 +local BANK_BAG_SPACING = 3 +local BANK_BAG_COUNT = 6 +local BANK_BAG_FIRST_ID = 5 +local BANK_BAG_LAST_ID = BANK_BAG_FIRST_ID + BANK_BAG_COUNT - 1 +local bankBagInvSlotCache = {} + +local function SafeBankUpdateLayout() + local ok, err = pcall(function() SFrames.Bags.Bank:UpdateLayout() end) + if not ok and err then + if bankLayoutErrorStamp ~= err then + bankLayoutErrorStamp = err + if SFrames and SFrames.Print then + SFrames:Print("闂備胶鍋撻崕濂搞€侀幋锔藉仼鐎光偓閸曨剚銆冮梺鍛婂浮閺€閬嶅蓟婵犲洦鐓ユ繛鍡樺俯閸? " .. tostring(err)) + end + end + end +end + +-- Override with clean localized error text. +SafeBankUpdateLayout = function() + local ok, err = pcall(function() SFrames.Bags.Bank:UpdateLayout() end) + if not ok and err then + if bankLayoutErrorStamp ~= err then + bankLayoutErrorStamp = err + if SFrames and SFrames.Print then + SFrames:Print(TEXT_LAYOUT_ERR .. tostring(err)) + end + end + end +end + +local function SaveBankFramePosition() + if not (SFBankFrame and SFramesDB and SFramesDB.Bags) then return end + local point, _, relPoint, x, y = SFBankFrame:GetPoint() + if not point or not relPoint then return end + SFramesDB.Bags.bankPosition = { + point = point, + relPoint = relPoint, + x = x or 0, + y = y or 0, + } +end + +local function ApplyBankFramePosition() + if not SFBankFrame then return end + SFBankFrame:ClearAllPoints() + + local pos = SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bankPosition + if pos and pos.point and pos.relPoint and type(pos.x) == "number" and type(pos.y) == "number" then + SFBankFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) + else + -- Default to right side; bag frame defaults to left side. + SFBankFrame:SetPoint("CENTER", UIParent, "CENTER", 360, 0) + end +end + +-- Bank sort: sorts main bank and bank bag containers +local function SortBank() + -- WoW 1.12 doesn't have SortBags() but we can do a basic consolidation + -- by using the container sort API if available + if SortBankBags then + SortBankBags() + end + if SortBags then + -- also sort player bags since items may move there + SortBags() + end +end + +local function SetTooltipFromBankContainerItem(bagID, slotID) + if not GameTooltip then return false end + GameTooltip:ClearLines() + + if GameTooltip.SetBagItem then + local ok = pcall(function() GameTooltip:SetBagItem(bagID, slotID) end) + if ok then + local left1 = _G["GameTooltipTextLeft1"] + if left1 and left1:GetText() and left1:GetText() ~= "" then + return true + end + end + end + + local link = GetContainerItemLink(bagID, slotID) + if link then + local ok = pcall(function() GameTooltip:SetHyperlink(link) end) + if ok then + local left1 = _G["GameTooltipTextLeft1"] + if left1 and left1:GetText() and left1:GetText() ~= "" then + return true + end + end + + local name = GetItemInfo(link) + if name and name ~= "" then + GameTooltip:SetText(name, 1, 1, 1) + return true + end + end + + return false +end + +local function HasTooltipText() + local left1 = _G["GameTooltipTextLeft1"] + return left1 and left1:GetText() and left1:GetText() ~= "" +end + +local function GetItemNameFromLink(link) + if type(link) ~= "string" or link == "" then + return nil + end + + local name = GetItemInfo(link) + if name and name ~= "" then + return name + end + + local _, _, parsed = string.find(link, "%[(.+)%]") + if parsed and parsed ~= "" then + return parsed + end + + return nil +end + +local function IsAsciiText(text) + if type(text) ~= "string" then return false end + return string.find(text, "[\128-\255]") == nil +end + +local function TextMatchesSearch(name, query) + if not query or query == "" then return true end + if not name or name == "" then return false end + + if string.find(name, query, 1, true) then + return true + end + + -- Keep Chinese/GBK safe: only do lowercase matching for pure ASCII strings. + if IsAsciiText(name) and IsAsciiText(query) then + return string.find(string.lower(name), string.lower(query), 1, true) ~= nil + end + + return false +end + +local function GetSafeCoinText(copper) + local value = tonumber(copper) or 0 + if value <= 0 then + return "0c" + end + + if GetCoinTextureString then + local ok, text = pcall(function() return GetCoinTextureString(value) end) + if ok and text and text ~= "" then + return text + end + end + + local g = math.floor(value / 10000) + local s = math.floor(math.mod(value, 10000) / 100) + local c = math.mod(value, 100) + return string.format("%dg %ds %dc", g, s, c) +end + +local function CreateCoinDisplay(parent, frameName) + local frame = CreateFrame("Frame", frameName, parent, "SmallMoneyFrameTemplate") + frame:SetFrameStrata(parent:GetFrameStrata()) + frame:SetFrameLevel(parent:GetFrameLevel() + 30) + frame:SetWidth(140) + frame:SetHeight(16) + return frame +end + +local function SetCoinDisplayMoney(display, copper) + if not display then return end + local value = tonumber(copper) or 0 + if value < 0 then value = 0 end + + if SmallMoneyFrame_SetAmount then + local ok = pcall(function() SmallMoneyFrame_SetAmount(display, value) end) + if ok then return end + end + + if MoneyFrame_Update and display.GetName then + local frameName = display:GetName() + if frameName and frameName ~= "" then + pcall(function() MoneyFrame_Update(frameName, value) end) + end + end +end + +local function IsValidBankInvSlot(id) + return type(id) == "number" and id >= 39 and id <= 74 +end + +local function GetMainBankInvSlotID(slotID) + if not slotID or slotID <= 0 then return nil end + + local liveBtn = _G["BankFrameItem" .. slotID] + if liveBtn and liveBtn.GetID then + local id = liveBtn:GetID() + if IsValidBankInvSlot(id) then + return id + end + end + + if not BankButtonIDToInvSlotID then return nil end + + local ok, invSlot = pcall(function() return BankButtonIDToInvSlotID(slotID) end) + if ok and IsValidBankInvSlot(invSlot) then + return invSlot + end + + ok, invSlot = pcall(function() return BankButtonIDToInvSlotID(slotID, 0) end) + if ok and IsValidBankInvSlot(invSlot) then + return invSlot + end + + ok, invSlot = pcall(function() return BankButtonIDToInvSlotID(slotID, 1) end) + if ok and IsValidBankInvSlot(invSlot) then + return invSlot + end + + return nil +end + +-- Override tooltip resolution with a stronger fallback chain for main bank slots. +SetTooltipFromBankContainerItem = function(bagID, slotID, cachedLink, cachedName) + if not GameTooltip then return false end + GameTooltip:ClearLines() + + if bagID == -1 and GameTooltip.SetInventoryItem then + local invSlot = GetMainBankInvSlotID(slotID) + if invSlot then + local ok = pcall(function() GameTooltip:SetInventoryItem("player", invSlot) end) + if ok and HasTooltipText() then + return true + end + end + end + + if GameTooltip.SetBagItem then + local ok = pcall(function() GameTooltip:SetBagItem(bagID, slotID) end) + if ok and HasTooltipText() then + return true + end + end + + local link = GetContainerItemLink(bagID, slotID) or cachedLink + if link then + local ok = pcall(function() GameTooltip:SetHyperlink(link) end) + if ok and HasTooltipText() then + return true + end + + local name = GetItemInfo(link) + if name and name ~= "" then + GameTooltip:SetText(name, 1, 1, 1) + return true + end + end + + if cachedName and cachedName ~= "" then + GameTooltip:SetText(cachedName, 1, 1, 1) + return true + end + + return false +end + +local function SafeGetInventoryItemLink(unit, invSlot) + if not invSlot then return nil end + local ok, link = pcall(function() return GetInventoryItemLink(unit, invSlot) end) + if ok then return link end + return nil +end + +local function SafeGetInventoryItemTexture(unit, invSlot) + if not invSlot then return nil end + local ok, tex = pcall(function() return GetInventoryItemTexture(unit, invSlot) end) + if ok then return tex end + return nil +end + +local function SafeGetInventorySlotByName(slotName) + if not GetInventorySlotInfo then return nil end + local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end) + if ok and type(slotID) == "number" and slotID > 0 then + return slotID + end + return nil +end + +local function IsPaperdollBagPlaceholder(tex) + if type(tex) ~= "string" then return false end + local lower = string.lower(tex) + lower = string.gsub(lower, "\\", "/") + + if string.find(lower, "ui-paperdoll-slot-bag", 1, true) then + return true + end + + if string.find(lower, "paperdoll", 1, true) and + string.find(lower, "slot", 1, true) and + string.find(lower, "bag", 1, true) then + return true + end + + return false +end + +local function IsDisabledIconTexture(tex) + if type(tex) ~= "string" then return false end + local lower = string.lower(tex) + lower = string.gsub(lower, "\\", "/") + return string.find(lower, "disabled", 1, true) ~= nil +end + +local function IsDefaultBankBagPlaceholder(tex) + if type(tex) ~= "string" then return false end + local lower = string.lower(tex) + lower = string.gsub(lower, "\\", "/") + if string.find(lower, "button-backpack-up", 1, true) then + return true + end + if string.find(lower, "button-backpack-disabled", 1, true) then + return true + end + return false +end + +local function IsUsableBankBagIconTexture(tex) + if type(tex) ~= "string" or tex == "" then + return false + end + if IsPaperdollBagPlaceholder(tex) then + return false + end + if IsDisabledIconTexture(tex) then + return false + end + if IsDefaultBankBagPlaceholder(tex) then + return false + end + return true +end + +local function GetIconFromItemLink(link) + if not link then return nil end + + local _, _, _, _, _, _, _, _, tex = GetItemInfo(link) + if tex then return tex end + + local _, _, itemID = string.find(link, "item:(%d+)") + if itemID then + local _, _, _, _, _, _, _, _, tex2 = GetItemInfo("item:" .. itemID) + if tex2 then return tex2 end + end + + return nil +end + +local function IsBagItemLink(link) + if type(link) ~= "string" or link == "" then + return false + end + + local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link) + return equipLoc == "INVTYPE_BAG" +end + +local function GetBagLinkState(link) + if type(link) ~= "string" or link == "" then + return "invalid" + end + + local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link) + if equipLoc == "INVTYPE_BAG" then + return "bag" + end + if equipLoc == nil or equipLoc == "" then + return "unknown" + end + return "invalid" +end + +local function AddInvSlotCandidate(candidates, seen, slotID) + if type(slotID) ~= "number" or slotID <= 0 then + return + end + if seen[slotID] then + return + end + seen[slotID] = true + table.insert(candidates, slotID) +end + +local function GetLiveBankBagButton(index) + local names = { + "BankFrameBag" .. index, + "BankFrameBag" .. index .. "Slot", + } + + for _, name in ipairs(names) do + local btn = _G[name] + if btn and btn.GetObjectType and btn:GetObjectType() == "Button" then + return btn + end + end + + return nil +end + +local function AddBankInvSlotCandidate(candidates, seen, slotID) + if type(slotID) ~= "number" or slotID <= 0 then + return + end + + -- Real bank bag equipment slots are not normal character inventory slots. + if slotID <= 23 then + return + end + + AddInvSlotCandidate(candidates, seen, slotID) +end + +local function IsBankBagInvSlotID(slotID) + return type(slotID) == "number" and slotID > 23 +end + +local function GetBankBagInvSlotID(index) + if type(index) ~= "number" or index <= 0 then + return nil + end + + local cached = bankBagInvSlotCache[index] + if type(cached) == "number" and cached > 23 then + return cached + end + bankBagInvSlotCache[index] = nil + + local bagID = index + (BANK_BAG_FIRST_ID - 1) + local function TryBankButtonID(buttonID, isBank) + if not BankButtonIDToInvSlotID then return nil end + local ok, slotID = pcall(function() return BankButtonIDToInvSlotID(buttonID, isBank) end) + if ok and IsBankBagInvSlotID(slotID) then + return slotID + end + return nil + end + + local function AcceptIfBankSlot(slotID) + if IsBankBagInvSlotID(slotID) then + return slotID + end + return nil + end + + -- Primary mapping: bag container id + isBank=1. + local slot = TryBankButtonID(bagID, 1) + if slot then + bankBagInvSlotCache[index] = slot + return slot + end + + -- Compatibility fallback: some clients may expect 1..N as first arg. + slot = TryBankButtonID(index, 1) + if slot then + bankBagInvSlotCache[index] = slot + return slot + end + + -- Only accept legacy guesses when they actually hold a bag item. + local function AcceptIfBag(slotID) + slotID = AcceptIfBankSlot(slotID) + if not slotID then return nil end + local link = SafeGetInventoryItemLink("player", slotID) + if link and IsBagItemLink(link) then + return slotID + end + return nil + end + + slot = TryBankButtonID(bagID, nil) + slot = AcceptIfBag(slot) + if slot then + bankBagInvSlotCache[index] = slot + return slot + end + + slot = TryBankButtonID(index, nil) + slot = AcceptIfBag(slot) + if slot then + bankBagInvSlotCache[index] = slot + return slot + end + + if ContainerIDToInventoryID then + local ok, result = pcall(function() return ContainerIDToInventoryID(bagID) end) + if ok then + slot = AcceptIfBankSlot(result) + if slot then + bankBagInvSlotCache[index] = slot + return slot + end + end + end + + -- BankBagSlotN is a stable bank bag equipment slot token and can be empty. + slot = AcceptIfBankSlot(SafeGetInventorySlotByName("BankBagSlot" .. index)) + if slot then + bankBagInvSlotCache[index] = slot + return slot + end + + -- Alternate token fallback used by a few legacy clients. + slot = AcceptIfBankSlot(SafeGetInventorySlotByName("BankBag" .. index)) + if slot then + bankBagInvSlotCache[index] = slot + return slot + end + + slot = AcceptIfBag(SafeGetInventorySlotByName("BankSlot" .. index)) + if slot then + bankBagInvSlotCache[index] = slot + return slot + end + + local liveBtn = GetLiveBankBagButton(index) + if liveBtn and liveBtn.GetID then + local btnID = liveBtn:GetID() + slot = TryBankButtonID(btnID, 1) or AcceptIfBankSlot(btnID) or AcceptIfBag(btnID) + if slot then + bankBagInvSlotCache[index] = slot + return slot + end + end + + return nil +end + +local function GetLiveBankBagIconTexture(index) + local btn = GetLiveBankBagButton(index) + if btn then + local icon = _G[btn:GetName() .. "IconTexture"] or _G[btn:GetName() .. "Icon"] + if icon and icon.GetTexture then + local tex = icon:GetTexture() + if type(tex) == "string" and tex ~= "" then + return tex + end + end + end + + local directIcon = _G["BankFrameBag" .. index .. "IconTexture"] or _G["BankFrameBag" .. index .. "Icon"] + if directIcon and directIcon.GetTexture then + local tex = directIcon:GetTexture() + if type(tex) == "string" and tex ~= "" then + return tex + end + end + + return nil +end + +local function IsBankBagSlotUnlocked(index) + local purchased = 0 + if GetNumBankSlots then + purchased = GetNumBankSlots() or 0 + end + return index <= purchased +end + +local function TryPurchaseBankSlot(index) + if not PurchaseSlot then return false end + if IsBankBagSlotUnlocked(index) then return false end + local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0 + local nextSlot = purchased + 1 + if index ~= nextSlot then return false end + local ok = pcall(function() PurchaseSlot() end) + return ok +end + +local function PlaceCursorItemInBankBagSlot(index) + if not CursorHasItem() then return false end + local invSlot = GetBankBagInvSlotID(index) + if not invSlot then return false end + + if EquipCursorItem then + local equipOK = pcall(function() EquipCursorItem(invSlot) end) + if equipOK and (not CursorHasItem()) then + return true + end + end + + -- This works for both placing and swapping bags in vanilla. + local ok = pcall(function() PickupBagFromSlot(invSlot) end) + if ok and (not CursorHasItem()) then + return true + end + + -- Fallback path for clients where PickupBagFromSlot doesn't place cursor item. + if PutItemInBag then + pcall(function() PutItemInBag(invSlot) end) + end + if CursorHasItem() then + pcall(function() PickupInventoryItem(invSlot) end) + end + return not CursorHasItem() +end + +local function GetOfflineBankSlotState(offlineDB, slotIndex) + local purchased = 0 + if offlineDB and type(offlineDB.bankSlots) == "number" then + purchased = math.max(0, math.floor(offlineDB.bankSlots)) + end + + if purchased <= 0 and offlineDB and offlineDB.bankBags then + for i = 1, BANK_BAG_COUNT do + local meta = offlineDB.bankBags[i] + local metaSize = 0 + if meta and type(meta.size) == "number" then + metaSize = math.max(0, math.floor(meta.size)) + end + if meta and (meta.unlocked or metaSize > 0) then + purchased = math.max(purchased, i) + end + end + end + + if purchased <= 0 and offlineDB and offlineDB.bank then + for i = 1, BANK_BAG_COUNT do + local bagData = offlineDB.bank[i + (BANK_BAG_FIRST_ID - 1)] + local size = 0 + if bagData and type(bagData.size) == "number" then + size = math.max(0, math.floor(bagData.size)) + end + if size > 0 then + purchased = math.max(purchased, i) + end + end + end + + local bagID = slotIndex + (BANK_BAG_FIRST_ID - 1) + local bagData = offlineDB and offlineDB.bank and offlineDB.bank[bagID] + local bagSlots = 0 + if bagData and type(bagData.size) == "number" then + bagSlots = math.max(0, math.floor(bagData.size)) + end + + local bagMeta = offlineDB and offlineDB.bankBags and offlineDB.bankBags[slotIndex] + if bagMeta and type(bagMeta.size) == "number" and bagMeta.size > bagSlots then + bagSlots = math.max(0, math.floor(bagMeta.size)) + end + + -- Migration fallback for old offline snapshots that did not store bankSlots: + -- any slot with positive container size implies this slot is available. + if bagSlots > 0 and slotIndex > purchased then + purchased = slotIndex + end + + local unlocked = (slotIndex <= purchased) + if (not unlocked) and bagSlots > 0 then + unlocked = true + end + if (not unlocked) and bagMeta and bagMeta.unlocked then + unlocked = true + end + + local link = nil + local tex = nil + local rawLink = bagMeta and bagMeta.link or nil + if bagSlots > 0 then + local linkState = GetBagLinkState(rawLink) + if linkState == "bag" or linkState == "unknown" then + link = rawLink + tex = bagMeta and bagMeta.texture or nil + end + end + + return unlocked, bagSlots, link, tex, purchased +end + +local function CreateBankBagButton(parent, index) + local btn = CreateFrame("Button", "SFramesBankBagBtn" .. index, parent) + btn:SetWidth(BANK_BAG_SIZE) + btn:SetHeight(BANK_BAG_SIZE) + btn.slotIndex = index + btn:SetFrameStrata(parent:GetFrameStrata()) + btn:SetFrameLevel(parent:GetFrameLevel() + 20) + + -- Rounded backdrop (matching bag slot style) + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 } + }) + btn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9) + btn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8) + + local icon = btn:CreateTexture(nil, "OVERLAY") + icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2) + icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:SetBlendMode("BLEND") + icon:SetVertexColor(1, 1, 1, 1) + icon:SetAlpha(1) + btn.icon = icon + + local lock = btn:CreateTexture(nil, "OVERLAY") + lock:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up") + lock:SetWidth(12) + lock:SetHeight(12) + lock:SetPoint("CENTER", btn, "CENTER", 0, 0) + lock:Hide() + btn.lockIcon = lock + + local highlight = btn:CreateTexture(nil, "HIGHLIGHT") + highlight:SetTexture("Interface\\Buttons\\ButtonHilight-Square") + highlight:SetBlendMode("ADD") + highlight:SetAllPoints(btn) + + btn:RegisterForClicks("LeftButtonUp", "RightButtonUp") + btn:RegisterForDrag("LeftButton") + + btn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:ClearLines() + if SFrames.Bags.Bank.isOffline then + local data = nil + if SFrames.Bags.Bank.offlineChar and SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then + data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar) + end + + local unlocked, bagSlots, bagLink = GetOfflineBankSlotState(data, this.slotIndex) + if unlocked then + local shown = false + if bagSlots > 0 and bagLink then + local _, _, itemStr = string.find(bagLink, "(item:[%-?%d:]+)") + local ok = false + if itemStr then + ok = pcall(function() GameTooltip:SetHyperlink(itemStr) end) + else + ok = pcall(function() GameTooltip:SetHyperlink(bagLink) end) + end + shown = ok and HasTooltipText() + + if not shown then + local name = GetItemNameFromLink(bagLink) + if name and name ~= "" then + GameTooltip:SetText(name, 1, 1, 1) + shown = true + end + end + end + + if not shown then + if bagSlots > 0 then + GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BANK_BAG, this.slotIndex, bagSlots, TEXT_SLOT_UNIT), 1, 1, 1) + else + GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BANK_BAG_SLOT, this.slotIndex, TEXT_EMPTY), 0.9, 0.9, 0.9) + end + end + GameTooltip:AddLine(TEXT_OFFLINE, 0.75, 0.75, 0.75) + else + GameTooltip:SetText(TEXT_LOCKED_BANK_SLOT, 1, 0.2, 0.2) + GameTooltip:AddLine(TEXT_OFFLINE, 0.75, 0.75, 0.75) + end + else + local unlocked = this.unlocked + if unlocked then + local invSlot = GetBankBagInvSlotID(this.slotIndex) + local bagSlots = GetContainerNumSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1)) or 0 + local shown = false + + if bagSlots > 0 and invSlot and GameTooltip.SetInventoryItem then + local ok = pcall(function() GameTooltip:SetInventoryItem("player", invSlot) end) + if ok and HasTooltipText() then + shown = true + end + end + + if not shown then + local link = SafeGetInventoryItemLink("player", invSlot) + if bagSlots > 0 and link then + local ok = pcall(function() GameTooltip:SetHyperlink(link) end) + if ok and HasTooltipText() then + shown = true + end + end + end + + if not shown then + if bagSlots > 0 then + GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BANK_BAG, this.slotIndex, bagSlots, TEXT_SLOT_UNIT), 1, 1, 1) + else + GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BANK_BAG_SLOT, this.slotIndex, TEXT_EMPTY), 0.9, 0.9, 0.9) + end + end + GameTooltip:AddLine(TEXT_DRAG_EQUIP, 0.7, 0.7, 0.7) + GameTooltip:AddLine(TEXT_RIGHT_PICKUP, 0.7, 0.7, 0.7) + else + GameTooltip:SetText(TEXT_LOCKED_BANK_SLOT, 1, 0.2, 0.2) + local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0 + local nextSlot = purchased + 1 + if this.slotIndex == nextSlot then + local cost = (GetBankSlotCost and GetBankSlotCost()) or 0 + if cost > 0 then + GameTooltip:AddLine(TEXT_CLICK_BUY, 1, 0.82, 0) + GameTooltip:AddLine(GetSafeCoinText(cost), 1, 0.82, 0) + else + GameTooltip:AddLine(TEXT_CLICK_BUY, 1, 0.82, 0) + end + else + GameTooltip:AddLine(TEXT_BUY_PREV_FIRST, 0.7, 0.7, 0.7) + end + end + end + if this.unlocked and SFBankFrame and SFBankFrame:IsVisible() then + if SFrames.Bags.Bank.isOffline then + local data = nil + if SFrames.Bags.Bank.offlineChar and SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then + data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar) + end + local _, offlineBagSlots = GetOfflineBankSlotState(data, this.slotIndex) + if offlineBagSlots > 0 then + SFrames.Bags.Bank:PreviewBankBagSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1)) + end + else + local bagSlots = GetContainerNumSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1)) or 0 + if bagSlots > 0 then + SFrames.Bags.Bank:PreviewBankBagSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1)) + end + end + end + GameTooltip:Show() + end) + btn:SetScript("OnLeave", function() + GameTooltip:Hide() + if SFBankFrame and SFBankFrame:IsVisible() then + SFrames.Bags.Bank:ClearBankBagPreview() + end + end) + + btn:SetScript("OnDragStart", function() + if SFrames.Bags.Bank.isOffline then return end + if not this.unlocked then return end + local invSlot = GetBankBagInvSlotID(this.slotIndex) + if invSlot then + pcall(function() PickupBagFromSlot(invSlot) end) + end + end) + + btn:SetScript("OnReceiveDrag", function() + if SFrames.Bags.Bank.isOffline then return end + if not CursorHasItem() then return end + + if this.unlocked then + PlaceCursorItemInBankBagSlot(this.slotIndex) + else + if TryPurchaseBankSlot(this.slotIndex) then + if IsBankBagSlotUnlocked(this.slotIndex) and CursorHasItem() then + PlaceCursorItemInBankBagSlot(this.slotIndex) + end + end + end + + SafeBankUpdateLayout() + end) + + btn:SetScript("OnClick", function() + if SFrames.Bags.Bank.isOffline then return end + + if CursorHasItem() then + if this.unlocked then + PlaceCursorItemInBankBagSlot(this.slotIndex) + else + if TryPurchaseBankSlot(this.slotIndex) and IsBankBagSlotUnlocked(this.slotIndex) then + PlaceCursorItemInBankBagSlot(this.slotIndex) + end + end + SafeBankUpdateLayout() + return + end + + if this.unlocked then + local invSlot = GetBankBagInvSlotID(this.slotIndex) + if invSlot then + pcall(function() PickupBagFromSlot(invSlot) end) + end + else + TryPurchaseBankSlot(this.slotIndex) + end + + SafeBankUpdateLayout() + end) + + return btn +end + +-- Create a single item slot button +local function CreateSlot(parent, id) + local button = CreateFrame("Button", "SFramesBankSlot" .. id, parent, "ItemButtonTemplate") + button:RegisterForClicks("LeftButtonUp", "RightButtonUp") + button:RegisterForDrag("LeftButton") + + -- Rounded backdrop style (matching CharacterPanel equipment slots) + local DEFAULT_BORDER = (_A and _A.slotBorder) or { 0.25, 0.25, 0.3, 0.8 } + button:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 18, + insets = { left = 2, right = 2, top = 2, bottom = 2 } + }) + button:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9) + button:SetBackdropBorderColor(DEFAULT_BORDER[1], DEFAULT_BORDER[2], DEFAULT_BORDER[3], DEFAULT_BORDER[4]) + + -- Inset icon within the rounded border + local icon = _G[button:GetName() .. "IconTexture"] + if icon then + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:ClearAllPoints() + icon:SetPoint("TOPLEFT", button, "TOPLEFT", 4, -4) + icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -4, 4) + end + + local qualGlow = button:CreateTexture(nil, "OVERLAY") + qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + qualGlow:SetBlendMode("ADD") + qualGlow:SetAlpha(0.8) + qualGlow:SetWidth(SLOT_SIZE * 1.9) + qualGlow:SetHeight(SLOT_SIZE * 1.9) + qualGlow:SetPoint("CENTER", button, "CENTER", 0, 0) + qualGlow:Hide() + button.qualGlow = qualGlow + + function button:SetBorderColor(r, g, b, a) + self.qualGlow:SetVertexColor(r, g, b) + self.qualGlow:Show() + self._qualityBorder = true + end + function button:ShowBorder() + self._qualityBorder = true + end + function button:HideBorder() + self.qualGlow:Hide() + self._qualityBorder = false + end + + -- Hide the ugly default rounded Blizzard border + local nt = _G[button:GetName() .. "NormalTexture"] + if nt then nt:SetTexture(nil) nt:Hide() end + + -- Grey item marker (a small coin/junk icon in the corner) + local junkIcon = button:CreateTexture(nil, "OVERLAY") + junkIcon:SetTexture("Interface\\Buttons\\UI-GroupLoot-Coin-Up") + junkIcon:SetWidth(14) + junkIcon:SetHeight(14) + junkIcon:SetPoint("TOPLEFT", button, "TOPLEFT", 1, -1) + junkIcon:Hide() + button.junkIcon = junkIcon + + local previewGlow = button:CreateTexture(nil, "OVERLAY") + previewGlow:SetTexture("Interface\\Buttons\\ButtonHilight-Square") + previewGlow:SetBlendMode("ADD") + previewGlow:SetAllPoints(button) + previewGlow:Hide() + button.previewGlow = previewGlow + + button:SetScript("OnEnter", function() + if this.bagID == nil or this.slotID == nil then return end + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:ClearLines() + + if SFrames.Bags.Bank.isOffline and SFrames.Bags.Bank.offlineChar then + local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar) + if data and data.bank and data.bank[this.bagID] and data.bank[this.bagID].items[this.slotID] then + local link = data.bank[this.bagID].items[this.slotID].link + local shown = false + if link then + local _, _, itemStr = string.find(link, "(item:[%-?%d:]+)") + local ok = false + if itemStr then + ok = pcall(function() GameTooltip:SetHyperlink(itemStr) end) + else + ok = pcall(function() GameTooltip:SetHyperlink(link) end) + end + shown = ok and HasTooltipText() + + if not shown then + local name = GetItemNameFromLink(link) + if name and name ~= "" then + GameTooltip:SetText(name, 1, 1, 1) + shown = true + end + end + + if not shown then + GameTooltip:SetText(TEXT_ITEM, 1, 1, 1) + end + else + GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65) + end + else + GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65) + end + else + local shown = SetTooltipFromBankContainerItem(this.bagID, this.slotID, this.itemLink, this.itemName) + if not shown then + GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65) + end + end + if IsControlKeyDown() then + ShowInspectCursor() + end + GameTooltip:Show() + end) + + button:SetScript("OnUpdate", function() + if GameTooltip:IsOwned(this) then + if IsControlKeyDown() then + if not this.controlDownLast then + this.controlDownLast = true + ShowInspectCursor() + end + else + if this.controlDownLast then + this.controlDownLast = false + ResetCursor() + end + end + end + end) + + button:SetScript("OnLeave", function() + GameTooltip:Hide() + ResetCursor() + end) + + local cooldown = CreateFrame("Model", button:GetName().."Cooldown", button, "CooldownFrameTemplate") + cooldown:SetAllPoints(button) + + function button:SplitStack(split) + if not split or split < 1 then return end + if self.bagID == nil or self.slotID == nil then return end + if self.bagID == -1 then + SplitContainerItem(-1, self.slotID, split) + else + SplitContainerItem(self.bagID, self.slotID, split) + end + end + + button:SetScript("OnClick", function() + local bagID = this.bagID + local slotID = this.slotID + local isOffline = SFrames.Bags.Bank.isOffline + + -- Helper: get item link for this slot (works both online and offline) + local function GetSlotLink() + if isOffline and SFrames.Bags.Bank.offlineChar then + local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar) + if data and data.bank and data.bank[bagID] and data.bank[bagID].items[slotID] then + return data.bank[bagID].items[slotID].link + end + return nil + end + if bagID == -1 then + return GetContainerItemLink(-1, slotID) + else + return GetContainerItemLink(bagID, slotID) + end + end + + if IsControlKeyDown() and arg1 == "LeftButton" then + local link = GetSlotLink() + if link and DressUpItemLink then + DressUpItemLink(link) + return + end + end + if IsShiftKeyDown() then + local eb = ChatFrameEditBox + if eb and eb.IsVisible and eb:IsVisible() then + local link = GetSlotLink() + if link then eb:Insert(link) end + return + end + if not isOffline and arg1 == "LeftButton" and (not CursorHasItem()) and OpenStackSplitFrame then + local _, itemCount = GetContainerItemInfo(bagID, slotID) + if itemCount and itemCount > 1 then + OpenStackSplitFrame(itemCount, this, "BOTTOMLEFT", "TOPLEFT") + return + end + end + end + + -- Block all other actions in offline mode + if isOffline then return end + + if arg1 == "RightButton" then + if AutoStoreBankItem then + local ok = pcall(function() AutoStoreBankItem(bagID, slotID) end) + if ok then return end + end + UseContainerItem(bagID, slotID) + else + if bagID == -1 then + PickupContainerItem(-1, slotID) + else + PickupContainerItem(bagID, slotID) + end + end + end) + + button:SetScript("OnDragStart", function() + if SFrames.Bags.Bank.isOffline then return end + if CursorHasItem() then return end + local bagID = this.bagID + local slotID = this.slotID + if bagID == -1 then + PickupContainerItem(-1, slotID) + else + PickupContainerItem(bagID, slotID) + end + end) + + button:SetScript("OnReceiveDrag", function() + if SFrames.Bags.Bank.isOffline then return end + if CursorHasItem() then + local bagID = this.bagID + local slotID = this.slotID + if bagID == -1 then + PickupContainerItem(-1, slotID) + else + PickupContainerItem(bagID, slotID) + end + end + end) + + return button +end + +function SFrames.Bags.Bank:PreviewBankBagSlots(targetBagID) + for _, btn in ipairs(ItemSlots) do + if btn and btn:IsShown() then + local icon = _G[btn:GetName() .. "IconTexture"] + local isMatch = (btn.bagID == targetBagID) + + if icon then + icon:SetVertexColor(1, 1, 1) + end + + if btn.previewGlow then + if isMatch then + btn.previewGlow:Show() + else + btn.previewGlow:Hide() + end + end + end + end +end + +function SFrames.Bags.Bank:ClearBankBagPreview() + for _, btn in ipairs(ItemSlots) do + if btn and btn:IsShown() then + local icon = _G[btn:GetName() .. "IconTexture"] + if icon then icon:SetVertexColor(1, 1, 1) end + if btn.previewGlow then btn.previewGlow:Hide() end + end + end +end + +function SFrames.Bags.Bank:UpdateBankBagButtons() + if not SFBankFrame then return end + + local isOffline = self.isOffline + local offlineDB = nil + if isOffline and self.offlineChar and SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then + offlineDB = SFrames.Bags.Offline:GetCharacterData(self.offlineChar) + if not offlineDB then + isOffline = false + self.isOffline = false + self.offlineChar = nil + end + end + + local purchased = 0 + if isOffline and offlineDB then + local _, _, _, _, offPurchased = GetOfflineBankSlotState(offlineDB, 1) + purchased = offPurchased or 0 + else + purchased = (GetNumBankSlots and GetNumBankSlots()) or 0 + end + local nextSlot = purchased + 1 + local placeholderTex = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag" + + for i = 1, BANK_BAG_COUNT do + local btn = BankBagButtons[i] + if btn then + local bagID = i + (BANK_BAG_FIRST_ID - 1) + local unlocked = false + local tex = nil + + if isOffline and offlineDB then + local bagSlots, offlineLink, offlineTex + unlocked, bagSlots, offlineLink, offlineTex = GetOfflineBankSlotState(offlineDB, i) + + if offlineLink then + tex = GetIconFromItemLink(offlineLink) + end + if (not tex) and offlineLink and IsUsableBankBagIconTexture(offlineTex) then + tex = offlineTex + end + if not tex then + tex = placeholderTex + end + else + local purchasedSlot = (i <= purchased) + local bagSlots = GetContainerNumSlots(bagID) or 0 + local invSlot = GetBankBagInvSlotID(i) + local invLink = SafeGetInventoryItemLink("player", invSlot) + local invTex = SafeGetInventoryItemTexture("player", invSlot) + local liveTex = GetLiveBankBagIconTexture(i) + + if invLink and IsBagItemLink(invLink) then + tex = GetIconFromItemLink(invLink) + if (not tex) and IsUsableBankBagIconTexture(invTex) then + tex = invTex + end + end + if (not tex) and IsUsableBankBagIconTexture(liveTex) then + tex = liveTex + end + + local hasBag = (tex ~= nil) or (bagSlots > 0) + unlocked = purchasedSlot or hasBag + + if not tex then + tex = placeholderTex + end + end + + btn.unlocked = unlocked + btn.icon:SetTexture(tex) + btn:Enable() + if btn.icon.SetDesaturated then + pcall(function() btn.icon:SetDesaturated(false) end) + end + if btn.bg then btn.bg:SetTexture(0, 0, 0, 0) end + btn:SetAlpha(1) + btn.icon:SetAlpha(1) + + if unlocked then + btn.icon:SetVertexColor(1, 1, 1, 1) + btn.lockIcon:Hide() + else + btn.icon:SetVertexColor(0.5, 0.5, 0.5, 1) + if (not isOffline) and i == nextSlot then + btn.lockIcon:Hide() + else + btn.lockIcon:Show() + end + end + + btn:Show() + end + end + + if SFBankFrame.purchaseBtn then + if isOffline then + SFBankFrame.purchaseBtn:Hide() + else + if nextSlot <= BANK_BAG_COUNT then + local cost = (GetBankSlotCost and GetBankSlotCost()) or 0 + SFBankFrame.purchaseBtn.cost = cost + SFBankFrame.purchaseBtn:Show() + else + SFBankFrame.purchaseBtn:Hide() + end + end + end +end + +-- Build/Update the item slot grid +function SFrames.Bags.Bank:UpdateLayout() + if not SFBankFrame then return end + + local cols = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bankColumns) or 12 + local spacing = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bankSpacing) or SPACING + spacing = tonumber(spacing) or SPACING + if spacing < 0 then spacing = 0 end + local slots = {} + + local isOffline = self.isOffline + local charName = self.offlineChar + local offlineDB = nil + if isOffline and charName then + offlineDB = SFrames.Bags.Offline:GetCharacterData(charName) + if not offlineDB then + isOffline = false + self.isOffline = false + self.offlineChar = nil + if SFBankFrame and SFBankFrame.RefreshCharacterSelectorText then + SFBankFrame.RefreshCharacterSelectorText() + end + end + end + + -- Update money display (online/offline) + if SFBankFrame.moneyFrame then + local copper = 0 + if isOffline and offlineDB then + copper = offlineDB.money or 0 + else + copper = GetMoney() + end + SetCoinDisplayMoney(SFBankFrame.moneyFrame, copper) + end + + local bankBags = { -1 } + for bag = BANK_BAG_FIRST_ID, BANK_BAG_LAST_ID do + table.insert(bankBags, bag) + end + for _, bag in ipairs(bankBags) do + local size = 0 + if isOffline and offlineDB then + if offlineDB.bank and offlineDB.bank[bag] then size = offlineDB.bank[bag].size end + else + if bag == -1 then + -- Try API first, fallback to hardcoded 24 + local apiSize = GetContainerNumSlots(-1) + size = (apiSize and apiSize > 0) and apiSize or 24 + else + size = GetContainerNumSlots(bag) + end + end + for slot = 1, size do + table.insert(slots, { bag = bag, slot = slot }) + end + end + + -- Apply search filter (hide non-matching items, show dim-greyed slots instead) + local searchFilter = bankSearchText or "" + if searchFilter ~= "" then + local filtered = {} + for _, meta in ipairs(slots) do + local link + if isOffline and offlineDB then + if offlineDB.bank and offlineDB.bank[meta.bag] and offlineDB.bank[meta.bag].items[meta.slot] then + link = offlineDB.bank[meta.bag].items[meta.slot].link + end + else + if meta.bag == -1 then + link = GetContainerItemLink(-1, meta.slot) + else + link = GetContainerItemLink(meta.bag, meta.slot) + end + end + -- Keep slot if it matches OR if it's empty (so empty slots remain in grid) + local keep = true + if link then + local name = GetItemInfo(link) + if not name then + local _, _, parsedName = string.find(link, "%[(.+)%]") + name = parsedName + end + keep = TextMatchesSearch(name, searchFilter) + end + if keep then + table.insert(filtered, meta) + end + end + slots = filtered + end + + local numSlots = table.getn(slots) + if numSlots == 0 then numSlots = 1 end -- Prevent zero-size window + local rows = math.ceil(numSlots / cols) + + -- Resize frame + local width = MARGIN * 2 + (cols * SLOT_SIZE) + math.max(0, (cols - 1)) * spacing + local height = MARGIN * 2 + TOP_OFFSET + (rows * SLOT_SIZE) + math.max(0, (rows - 1)) * spacing + BOTTOM_OFFSET + SFBankFrame:SetWidth(math.max(width, 160)) + SFBankFrame:SetHeight(height) + + -- Position & update slots + for i, meta in ipairs(slots) do + local btn = ItemSlots[i] + if not btn then + btn = CreateSlot(SFBankFrame, i) + ItemSlots[i] = btn + end + + btn.bagID = meta.bag + btn.slotID = meta.slot + + local row = math.floor((i - 1) / cols) + local col = math.mod((i - 1), cols) + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", SFBankFrame, "TOPLEFT", + MARGIN + col * (SLOT_SIZE + spacing), + -(MARGIN + TOP_OFFSET + row * (SLOT_SIZE + spacing))) + + -- Fetch item info + local texture, count, quality, link + if isOffline and offlineDB then + if offlineDB.bank and offlineDB.bank[meta.bag] and offlineDB.bank[meta.bag].items[meta.slot] then + local item = offlineDB.bank[meta.bag].items[meta.slot] + texture = item.texture + count = item.count + quality = item.quality + link = item.link + end + else + if meta.bag == -1 then + -- Main bank slots: GetContainerItemInfo(-1, slot) is the correct API + -- This requires the bank to already be open and data populated (0.5s delay ensures this) + local t, c, _, q = GetContainerItemInfo(-1, meta.slot) + texture = t; count = c; quality = q + link = GetContainerItemLink(-1, meta.slot) + else + local t, c, _, q = GetContainerItemInfo(meta.bag, meta.slot) + texture = t; count = c; quality = q + link = GetContainerItemLink(meta.bag, meta.slot) + end + end + + btn.itemLink = link + if link then + btn.itemName = GetItemInfo(link) + else + btn.itemName = nil + end + + SetItemButtonTexture(btn, texture) + SetItemButtonCount(btn, count) + local iconTex = _G[btn:GetName() .. "IconTexture"] + if iconTex then iconTex:SetVertexColor(1, 1, 1) end + if btn.previewGlow then btn.previewGlow:Hide() end + + -- Quality border & Grey marker + btn:HideBorder() + btn.junkIcon:Hide() + + if link then + -- Safest 1.12 generic approach: Read the color straight out of the hyperlink! + local _, _, hex = string.find(link, "|c(%x+)|H") + local parsedColor = false + + if hex and string.len(hex) == 8 then + local hexLower = string.lower(hex) + if hexLower == "ff9d9d9d" then + -- Poor / Grey Item + btn:SetBorderColor(0.5, 0.5, 0.5, 1) + btn:ShowBorder() + btn.junkIcon:Show() + parsedColor = true + elseif hexLower == "ffffffff" then + -- Common / White Item + parsedColor = true + else + -- Green, Blue, Purple, Orange + local r = tonumber(string.sub(hex, 3, 4), 16) / 255 + local g = tonumber(string.sub(hex, 5, 6), 16) / 255 + local b = tonumber(string.sub(hex, 7, 8), 16) / 255 + btn:SetBorderColor(r, g, b, 1) + btn:ShowBorder() + parsedColor = true + end + end + + -- Fallback + if not parsedColor then + local q = quality + if not q then + local _, _, itemString = string.find(link, "item:(%d+)") + if itemString then + local _, _, scanRarity = GetItemInfo("item:" .. itemString) + q = scanRarity + end + end + + if q then + if q == 0 then + btn:SetBorderColor(0.5, 0.5, 0.5, 1) + btn:ShowBorder() + btn.junkIcon:Show() + elseif q > 1 then + local r, g, b = GetItemQualityColor(q) + btn:SetBorderColor(r, g, b, 1) + btn:ShowBorder() + end + end + end + end + + -- Cooldowns + local cooldown = _G[btn:GetName() .. "Cooldown"] + if cooldown then + if isOffline then + cooldown:Hide() + else + local start, duration, enable = GetContainerItemCooldown(meta.bag, meta.slot) + if start and duration and start > 0 and duration > 0 then + CooldownFrame_SetTimer(cooldown, start, duration, enable) + cooldown:Show() + else + cooldown:Hide() + end + end + end + + btn:Show() + end + + -- Hide excess buttons + for i = numSlots + 1, table.getn(ItemSlots) do + if ItemSlots[i] then ItemSlots[i]:Hide() end + end + + self:UpdateBankBagButtons() +end + +-- Main frame initialization (called once after PLAYER_LOGIN) +function SFrames.Bags.Bank:Initialize() + if SFBankFrame then return end + + SFBankFrame = CreateFrame("Frame", "SFramesBankFrame", UIParent) + SFBankFrame:SetWidth(420) + SFBankFrame:SetHeight(200) + SFBankFrame:SetFrameStrata("HIGH") + SFBankFrame:SetToplevel(true) + SFBankFrame:EnableMouse(true) + SFBankFrame:SetMovable(true) + SFBankFrame:SetClampedToScreen(true) + SFBankFrame:RegisterForDrag("LeftButton") + SFBankFrame:SetScript("OnDragStart", function() this:StartMoving() end) + SFBankFrame:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + SaveBankFramePosition() + end) + ApplyBankFramePosition() + tinsert(UISpecialFrames, "SFramesBankFrame") + SFBankFrame:Hide() + + -- ESC menu style rounded backdrop + SFBankFrame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + local _A = SFrames.ActiveTheme + if _A and _A.panelBg then + SFBankFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4] or 0.95) + SFBankFrame:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4] or 0.9) + else + SFBankFrame:SetBackdropColor(0.12, 0.06, 0.10, 0.95) + SFBankFrame:SetBackdropBorderColor(0.55, 0.30, 0.42, 0.9) + end + local bankShadow = CreateFrame("Frame", nil, SFBankFrame) + bankShadow:SetPoint("TOPLEFT", SFBankFrame, "TOPLEFT", -5, 5) + bankShadow:SetPoint("BOTTOMRIGHT", SFBankFrame, "BOTTOMRIGHT", 5, -5) + bankShadow:SetFrameLevel(math.max(SFBankFrame:GetFrameLevel() - 1, 0)) + bankShadow:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + bankShadow:SetBackdropColor(0, 0, 0, 0.55) + bankShadow:SetBackdropBorderColor(0, 0, 0, 0.4) + local scale = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.bankScale) == "number" and SFramesDB.Bags.bankScale) or 0.85 + SFBankFrame:SetScale(scale) + + local bankTitleIco = SFrames:CreateIcon(SFBankFrame, "gold", 14) + bankTitleIco:SetDrawLayer("OVERLAY") + bankTitleIco:SetPoint("TOPLEFT", SFBankFrame, "TOPLEFT", 10, -7) + bankTitleIco:SetVertexColor(_A.title[1], _A.title[2], _A.title[3]) + + local titleFS = SFrames:CreateFontString(SFBankFrame, 12, "LEFT") + titleFS:SetPoint("LEFT", bankTitleIco, "RIGHT", 4, 0) + titleFS:SetText(TEXT_BANK_TITLE) + titleFS:SetTextColor(_A.title[1], _A.title[2], _A.title[3]) + SFBankFrame.title = titleFS + + -- Close button + local closeBtn = CreateFrame("Button", "SFramesBankClose", SFBankFrame, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", SFBankFrame, "TOPRIGHT", 0, 0) + closeBtn:SetScript("OnClick", function() SFrames.Bags.Bank:Close() end) + + -- Money display + SFBankFrame.moneyFrame = CreateCoinDisplay(SFBankFrame, "SFramesBankMoneyFrame") + -- Temporary anchor; final anchor is set after purchase button is created. + SFBankFrame.moneyFrame:SetPoint("RIGHT", SFBankFrame, "BOTTOMRIGHT", -8, 17) + SetCoinDisplayMoney(SFBankFrame.moneyFrame, 0) + + -- Search bar (row 2: below title) + local searchEB = CreateFrame("EditBox", "SFramesBankSearchBox", SFBankFrame, "InputBoxTemplate") + searchEB:SetWidth(120) + searchEB:SetHeight(18) + searchEB:SetPoint("TOPLEFT", SFBankFrame, "TOPLEFT", 10, -27) + searchEB:SetAutoFocus(false) + searchEB:SetScript("OnEnterPressed", function() this:ClearFocus() end) + searchEB:SetScript("OnEscapePressed", function() + this:ClearFocus() + this:SetText("") + bankSearchText = "" + SafeBankUpdateLayout() + end) + searchEB:SetScript("OnTextChanged", function() + bankSearchText = this:GetText() or "" + SafeBankUpdateLayout() + end) + SFBankFrame.searchEB = searchEB + + local function CreateHeaderIconButton(name, parent, iconPath) + local btn = CreateFrame("Button", name, parent) + btn:SetWidth(18) + btn:SetHeight(18) + btn:SetFrameStrata(parent:GetFrameStrata()) + btn:SetFrameLevel(parent:GetFrameLevel() + 45) + + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 } + }) + btn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.86) + btn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8) + + local icon = btn:CreateTexture(nil, "ARTWORK") + icon:SetTexture(iconPath or "Interface\\Icons\\INV_Misc_QuestionMark") + icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2) + icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + btn.icon = icon + + local hl = btn:CreateTexture(nil, "HIGHLIGHT") + hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square") + hl:SetBlendMode("ADD") + hl:SetAllPoints(btn) + + return btn + end + + -- Sort button (icon) + local sortBtn = CreateHeaderIconButton("SFramesBankSortBtn", SFBankFrame, "Interface\\Icons\\INV_Misc_Note_05") + sortBtn:SetPoint("LEFT", searchEB, "RIGHT", 6, 0) + SFrames:SetIcon(sortBtn.icon, "gold") + sortBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(TEXT_SORT, 1, 1, 1) + GameTooltip:Show() + end) + sortBtn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + sortBtn:SetScript("OnClick", function() + if SFrames.Bags.Sort and SFrames.Bags.Sort.StartBank then + SFrames.Bags.Sort:StartBank() + return + end + + -- Fallback for environments with a native bank sort API. + SortBank() + + local elapsed = 0 + local t = CreateFrame("Frame") + t:SetScript("OnUpdate", function() + elapsed = elapsed + arg1 + if elapsed >= 0.3 then + this:SetScript("OnUpdate", nil) + SafeBankUpdateLayout() + end + end) + end) + SFBankFrame.sortBtn = sortBtn + + -- Bank bag slots (equip/unequip + purchase flow) + for i = 1, BANK_BAG_COUNT do + local bagBtn = CreateBankBagButton(SFBankFrame, i) + if i == 1 then + bagBtn:SetPoint("BOTTOMLEFT", SFBankFrame, "BOTTOMLEFT", 8, 6) + else + bagBtn:SetPoint("LEFT", BankBagButtons[i - 1], "RIGHT", BANK_BAG_SPACING, 0) + end + BankBagButtons[i] = bagBtn + end + + local purchaseBtn = CreateHeaderIconButton("SFramesBankPurchaseBtn", SFBankFrame, "Interface\\Icons\\INV_Misc_Coin_01") + purchaseBtn:SetWidth(BANK_BAG_SIZE) + purchaseBtn:SetHeight(BANK_BAG_SIZE) + purchaseBtn:SetPoint("BOTTOMRIGHT", SFBankFrame, "BOTTOMRIGHT", -8, 6) + purchaseBtn.cost = 0 + purchaseBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:ClearLines() + GameTooltip:SetText(TEXT_BUY_SLOT, 1, 1, 1) + if SFrames.Bags.Bank.isOffline then + GameTooltip:AddLine(TEXT_UNAVAILABLE_OFFLINE, 0.75, 0.75, 0.75) + else + local cost = this.cost or 0 + if cost > 0 then + GameTooltip:AddLine(GetSafeCoinText(cost), 1, 0.82, 0) + end + GameTooltip:AddLine(TEXT_CLICK_BUY, 0.75, 0.75, 0.75) + end + GameTooltip:Show() + end) + purchaseBtn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + purchaseBtn:SetScript("OnClick", function() + if SFrames.Bags.Bank.isOffline then return end + if PurchaseSlot then + pcall(function() PurchaseSlot() end) + SafeBankUpdateLayout() + end + end) + SFBankFrame.purchaseBtn = purchaseBtn + + -- Keep bank money display on the same bottom row as bank bag controls. + if SFBankFrame.moneyFrame then + SFBankFrame.moneyFrame:ClearAllPoints() + SFBankFrame.moneyFrame:SetPoint("RIGHT", purchaseBtn, "LEFT", -6, 0) + end + + -- Character selector: button-triggered dropdown (stable text, avoids UIDropDown label glitches) + local function GetCurrentCharacterName() + local live = UnitName("player") + if type(live) == "string" and live ~= "" then + return live + end + local cached = SFrames.Bags.Offline:GetCurrentPlayerName() + if type(cached) == "string" and cached ~= "" then + return cached + end + return TEXT_CHARACTER + end + + local charBtn = CreateHeaderIconButton("SFramesBankCharBtn", SFBankFrame, CHARACTER_SELECTOR_ICON) + charBtn:SetPoint("TOPRIGHT", SFBankFrame, "TOPRIGHT", -8, -26) + SFBankFrame.charSelectBtn = charBtn + + local dd = CreateFrame("Frame", "SFramesBankOfflineDD", SFBankFrame, "UIDropDownMenuTemplate") + dd:Hide() + dd:SetPoint("TOPRIGHT", charBtn, "BOTTOMRIGHT", 0, 0) + + local function RefreshCharacterSelectorText() + if not SFBankFrame or not SFBankFrame.charSelectBtn then return end + if SFrames.Bags.Bank.isOffline and SFrames.Bags.Bank.offlineChar then + SFBankFrame.charSelectorLabel = SFrames.Bags.Bank.offlineChar .. " (" .. TEXT_OFFLINE .. ")" + else + SFBankFrame.charSelectorLabel = GetCurrentCharacterName() .. " (" .. TEXT_ONLINE .. ")" + end + end + SFBankFrame.RefreshCharacterSelectorText = RefreshCharacterSelectorText + + local function OnSelect() + local char = this.value + if char == "CURRENT" then + SFrames.Bags.Bank.isOffline = false + SFrames.Bags.Bank.offlineChar = nil + else + SFrames.Bags.Bank.isOffline = true + SFrames.Bags.Bank.offlineChar = char + end + RefreshCharacterSelectorText() + SafeBankUpdateLayout() + end + + UIDropDownMenu_Initialize(dd, function() + local info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {} + local currentName = GetCurrentCharacterName() + info.text = currentName .. " (" .. TEXT_ONLINE .. ")" + info.value = "CURRENT" + info.func = OnSelect + info.checked = (not SFrames.Bags.Bank.isOffline) + UIDropDownMenu_AddButton(info) + + local chars = SFrames.Bags.Offline:GetCharacterList() + table.sort(chars) + for _, char in ipairs(chars) do + if char ~= currentName then + info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {} + info.text = char .. " (" .. TEXT_OFFLINE .. ")" + info.value = char + info.func = OnSelect + info.checked = SFrames.Bags.Bank.isOffline and SFrames.Bags.Bank.offlineChar == char + UIDropDownMenu_AddButton(info) + end + end + end) + + charBtn:SetScript("OnClick", function() + ToggleDropDownMenu(1, nil, dd, this, 0, 0) + end) + charBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(SFBankFrame.charSelectorLabel or TEXT_CHARACTER, 1, 1, 1) + GameTooltip:Show() + end) + charBtn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + RefreshCharacterSelectorText() + + -- Use BANKFRAME_OPENED / CLOSED events (same as Guda) + -- IMPORTANT: WoW 1.12 needs a short delay before GetContainerItemInfo(-1,slot) + -- returns valid data after BANKFRAME_OPENED fires. + local eventFrame = CreateFrame("Frame") + eventFrame:RegisterEvent("BANKFRAME_OPENED") + eventFrame:RegisterEvent("BANKFRAME_CLOSED") + eventFrame:RegisterEvent("BAG_UPDATE") + eventFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED") + eventFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED") + eventFrame:RegisterEvent("PLAYER_MONEY") + eventFrame:SetScript("OnEvent", function() + if not (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.enable) then return end + + if event == "BANKFRAME_OPENED" then + -- CRITICAL: Do NOT call BankFrame:Hide() here! + -- BankFrame has an OnHide script that calls CloseBankFrame(), which + -- immediately closes the bank session and makes GetContainerItemInfo(-1,slot) return nil. + -- Instead, move BankFrame off-screen so the session stays open but it's invisible. + local bf = _G["BankFrame"] + if bf then + bf:ClearAllPoints() + bf:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -5000, 5000) + bf:EnableMouse(false) -- Prevent accidental clicks + end + -- Open once; Open() already performs a delayed refresh. + SFrames.Bags.Bank:Open() + + elseif event == "BANKFRAME_CLOSED" then + if not isClosing then + SFrames.Bags.Bank:Close() + end + + elseif event == "BAG_UPDATE" or event == "PLAYERBANKSLOTS_CHANGED" or event == "PLAYERBANKBAGSLOTS_CHANGED" or event == "PLAYER_MONEY" then + if event == "PLAYERBANKBAGSLOTS_CHANGED" then + bankBagInvSlotCache = {} + elseif event == "BAG_UPDATE" and type(arg1) == "number" and arg1 >= BANK_BAG_FIRST_ID and arg1 <= BANK_BAG_LAST_ID then + bankBagInvSlotCache = {} + end + if SFBankFrame:IsVisible() and not SFrames.Bags.Bank.isOffline then + SafeBankUpdateLayout() + end + end + end) +end + +function SFrames.Bags.Bank:Open() + if not SFBankFrame then return end + self.isOffline = false + self.offlineChar = nil + bankBagInvSlotCache = {} + if SFBankFrame and SFBankFrame.RefreshCharacterSelectorText then + SFBankFrame.RefreshCharacterSelectorText() + end + + -- Bank-open layout: bank on left, bags on right. + if SFBankFrame then + SFBankFrame:ClearAllPoints() + SFBankFrame:SetPoint("CENTER", UIParent, "CENTER", -360, 0) + end + local bagFrame = _G["SFramesBagFrame"] + if bagFrame then + bagFrame:ClearAllPoints() + bagFrame:SetPoint("CENTER", UIParent, "CENTER", 360, 0) + end + + SFBankFrame:Show() + + local elapsed = 0 + local nextRefresh = 1 + local refreshPoints = { 0.06, 0.20, 0.45, 0.85, 1.30 } + local refreshTimer = CreateFrame("Frame") + refreshTimer:SetScript("OnUpdate", function() + elapsed = elapsed + arg1 + if nextRefresh <= table.getn(refreshPoints) and elapsed >= refreshPoints[nextRefresh] then + if SFBankFrame:IsVisible() and not SFrames.Bags.Bank.isOffline then + SafeBankUpdateLayout() + end + nextRefresh = nextRefresh + 1 + end + if nextRefresh > table.getn(refreshPoints) then + this:SetScript("OnUpdate", nil) + end + end) + + -- Keep bank bag state synced: some clients delay slot/container updates. + local syncFrame = CreateFrame("Frame") + syncFrame.timer = 0 + syncFrame:SetScript("OnUpdate", function() + if not (SFBankFrame and SFBankFrame:IsVisible()) then + this.timer = 0 + return + end + if SFrames.Bags.Bank.isOffline then + this.timer = 0 + return + end + + this.timer = this.timer + (arg1 or 0) + if this.timer >= 0.5 then + this.timer = 0 + SFrames.Bags.Bank:UpdateBankBagButtons() + end + end) +end + +function SFrames.Bags.Bank:OpenOffline(charName) + if not SFBankFrame then + if self.Initialize then + self:Initialize() + end + end + if not SFBankFrame then + return false + end + + local targetChar = nil + if type(charName) == "string" and charName ~= "" then + targetChar = charName + elseif SFrames.Bags.Offline and SFrames.Bags.Offline.GetCurrentPlayerName then + targetChar = SFrames.Bags.Offline:GetCurrentPlayerName() + end + + if not targetChar or targetChar == "" then + if SFrames and SFrames.Print then + SFrames:Print(TEXT_UNAVAILABLE_OFFLINE) + end + return false + end + + local data = nil + if SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then + data = SFrames.Bags.Offline:GetCharacterData(targetChar) + end + if not data then + if SFrames and SFrames.Print then + SFrames:Print(TEXT_UNAVAILABLE_OFFLINE) + end + return false + end + + self.isOffline = true + self.offlineChar = targetChar + bankBagInvSlotCache = {} + + if SFBankFrame.RefreshCharacterSelectorText then + SFBankFrame.RefreshCharacterSelectorText() + end + + -- Match live-bank layout so bag and bank are shown side by side. + SFBankFrame:ClearAllPoints() + SFBankFrame:SetPoint("CENTER", UIParent, "CENTER", -360, 0) + local bagFrame = _G["SFramesBagFrame"] + if bagFrame then + bagFrame:ClearAllPoints() + bagFrame:SetPoint("CENTER", UIParent, "CENTER", 360, 0) + end + + SFBankFrame:Show() + SafeBankUpdateLayout() + return true +end + +function SFrames.Bags.Bank:Close() + if isClosing then return end -- Already closing, prevent recursion + isClosing = true + local wasOffline = self.isOffline + if SFBankFrame then SFBankFrame:Hide() end + -- Restore BankFrame so WoW's close sequence works properly, + -- then let CloseBankFrame() trigger its own OnHide naturally. + local bf = _G["BankFrame"] + if bf then + bf:EnableMouse(true) + bf:ClearAllPoints() + bf:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + end + if (not wasOffline) and CloseBankFrame then + CloseBankFrame() -- Tell server to close bank session (triggers BankFrame OnHide and closes it) + end + isClosing = false +end + +function SFrames.Bags.Bank:Toggle() + if SFBankFrame and SFBankFrame:IsVisible() then + self:Close() + else + self:Open() + end +end + +SLASH_NANAMIBANKDBG1 = "/nanamibankdbg" +SlashCmdList["NANAMIBANKDBG"] = function() + if not (SFrames and SFrames.Print) then return end + + local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0 + SFrames:Print("BankDbg purchased=" .. tostring(purchased)) + + for i = 1, BANK_BAG_COUNT do + local bagID = i + (BANK_BAG_FIRST_ID - 1) + local bagSlots = GetContainerNumSlots(bagID) or 0 + local invSlot = GetBankBagInvSlotID(i) + local link = invSlot and SafeGetInventoryItemLink("player", invSlot) or nil + local isBag = link and IsBagItemLink(link) + local liveTex = GetLiveBankBagIconTexture(i) + local unlocked = IsBankBagSlotUnlocked(i) + + local mapBag1 = nil + local mapIdx1 = nil + if BankButtonIDToInvSlotID then + local okA, resA = pcall(function() return BankButtonIDToInvSlotID(bagID, 1) end) + if okA then mapBag1 = resA end + local okB, resB = pcall(function() return BankButtonIDToInvSlotID(i, 1) end) + if okB then mapIdx1 = resB end + end + + local line = + "slot" .. i .. + " bagID=" .. tostring(bagID) .. + " size=" .. tostring(bagSlots) .. + " unlocked=" .. tostring(unlocked) .. + " invSlot=" .. tostring(invSlot) .. + " link=" .. tostring(link) .. + " isBag=" .. tostring(isBag) .. + " liveTex=" .. tostring(liveTex) .. + " map(bag,1)=" .. tostring(mapBag1) .. + " map(idx,1)=" .. tostring(mapIdx1) + + SFrames:Print(line) + end +end diff --git a/Bags/Container.lua b/Bags/Container.lua new file mode 100644 index 0000000..e951b7a --- /dev/null +++ b/Bags/Container.lua @@ -0,0 +1,1619 @@ +-------------------------------------------------------------------------------- +-- S-Frames: Bag Container UI (Bags/Container.lua) +-------------------------------------------------------------------------------- + +SFrames.Bags.Container = {} + +local SLOT_SIZE = 36 +local SPACING = 6 +local MARGIN = 10 +local TOP_OFFSET = 52 -- Space for title + search bar + gold +local TEXT_EMPTY = "\231\169\186" +local TEXT_ITEM = "\231\137\169\229\147\129" +local TEXT_BACKPACK = "\232\131\140\229\140\133" +local TEXT_BAGS_TITLE = "\232\131\140\229\140\133" +local TEXT_BANK_TITLE = "\233\147\182\232\161\140" +local TEXT_SORT = "\230\149\180\231\144\134" +local TEXT_SETTINGS = "\232\174\190\231\189\174" +local TEXT_BAG_SLOT = "\232\131\140\229\140\133\230\167\189" +local TEXT_BAG_EMPTY = "\231\169\186" +local TEXT_BAG_SLOTS = "\230\160\188" +local TEXT_EQUIPPED_BAG = "\229\183\178\232\163\133\229\164\135\232\131\140\229\140\133" +local TEXT_HS_USE = "\231\130\185\229\135\187\228\189\191\231\148\168\231\130\137\231\159\179" +local TEXT_HS_NOT_FOUND = "\232\131\140\229\140\133\228\184\173\230\156\170\230\137\190\229\136\176\231\130\137\231\159\179\227\128\130" +local TEXT_CHARACTER = "\232\167\146\232\137\178" +local TEXT_ONLINE = "\229\156\168\231\186\191" +local TEXT_OFFLINE = "\231\166\187\231\186\191" +local PANEL_BG_ALPHA = 0.55 +local SLOT_BG_ALPHA = 0.22 +local CHARACTER_SELECTOR_ICON = "Interface\\CHARACTERFRAME\\TemporaryPortrait-Female-Human" + +local _A = SFrames.ActiveTheme + +local BagFrame = nil +local ItemSlots = {} +local playerBagInvSlots = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 } + +local function IsLiveBankOpen() + local bankFrame = _G["SFramesBankFrame"] + if not bankFrame or not bankFrame:IsVisible() then return false end + if not SFrames.Bags.Bank then return false end + return not SFrames.Bags.Bank.isOffline +end + +local function SetTooltipFromContainerItem(bagID, slotID) + if not GameTooltip then return false end + GameTooltip:ClearLines() + + if GameTooltip.SetBagItem then + local ok = pcall(function() GameTooltip:SetBagItem(bagID, slotID) end) + if ok then + local left1 = _G["GameTooltipTextLeft1"] + if left1 and left1:GetText() and left1:GetText() ~= "" then + return true + end + end + end + + local link = GetContainerItemLink(bagID, slotID) + if link then + local ok = pcall(function() GameTooltip:SetHyperlink(link) end) + if ok then + local left1 = _G["GameTooltipTextLeft1"] + if left1 and left1:GetText() and left1:GetText() ~= "" then + return true + end + end + + local name = GetItemInfo(link) + if name and name ~= "" then + GameTooltip:SetText(name, 1, 1, 1) + return true + end + end + + return false +end + +local function HasTooltipText() + local left1 = _G["GameTooltipTextLeft1"] + return left1 and left1:GetText() and left1:GetText() ~= "" +end + +local function GetItemNameFromLink(link) + if type(link) ~= "string" or link == "" then + return nil + end + + local name = GetItemInfo(link) + if name and name ~= "" then + return name + end + + local _, _, parsed = string.find(link, "%[(.+)%]") + if parsed and parsed ~= "" then + return parsed + end + + return nil +end + +local function ShowMerchantCursorForSlot(button) + if not button then return end + if button.bagID == nil or button.slotID == nil then return end + if SFrames.Bags.Container and SFrames.Bags.Container.isOffline then return end + if not ((MerchantFrame and MerchantFrame:IsVisible()) or (SFramesMerchantFrame and SFramesMerchantFrame:IsVisible())) then return end + + local texture, _, locked = GetContainerItemInfo(button.bagID, button.slotID) + if not texture or locked then return end + + -- Prefer container-aware API when available. + if ShowContainerSellCursor then + local ok = pcall(function() + ShowContainerSellCursor(button.bagID, button.slotID) + end) + if ok then return end + end + + -- Fallback to generic merchant sell cursor. + if ShowMerchantSellCursor then + local ok = pcall(function() + ShowMerchantSellCursor() + end) + if ok then return end + end + + -- Final fallback for clients lacking both APIs. + if SetCursor then + pcall(function() + SetCursor("BUY_CURSOR") + end) + end +end + +local function ResolvePlayerBagInvSlots() + local fallback = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 } + for bagIndex = 1, 4 do + local invSlot = nil + local liveBtn = _G["CharacterBag" .. (bagIndex - 1) .. "Slot"] + + -- Most reliable when Blizzard character UI is loaded. + if liveBtn and liveBtn.GetID then + local id = liveBtn:GetID() + if type(id) == "number" and id > 0 then + invSlot = id + end + end + + if (not invSlot) and ContainerIDToInventoryID then + local ok, slotID = pcall(function() return ContainerIDToInventoryID(bagIndex) end) + if ok and type(slotID) == "number" and slotID > 0 then + invSlot = slotID + end + end + + if (not invSlot) and GetInventorySlotInfo then + local slotNames = { + "Bag" .. (bagIndex - 1) .. "Slot", + "Bag" .. bagIndex .. "Slot", + "CharacterBag" .. (bagIndex - 1) .. "Slot", + } + for _, slotName in ipairs(slotNames) do + local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end) + if ok and type(slotID) == "number" and slotID > 0 then + invSlot = slotID + break + end + end + end + + playerBagInvSlots[bagIndex] = invSlot or fallback[bagIndex] + end +end + +local function GetLivePlayerBagIconTexture(bagIndex) + local btn = _G["CharacterBag" .. (bagIndex - 1) .. "Slot"] + if not btn then return nil end + + local icon = _G[btn:GetName() .. "IconTexture"] or _G[btn:GetName() .. "Icon"] + if icon and icon.GetTexture then + local tex = icon:GetTexture() + if tex and tex ~= "" then + return tex + end + end + + return nil +end + +local function IsDisabledIconTexture(tex) + if type(tex) ~= "string" then return false end + local lower = string.lower(tex) + lower = string.gsub(lower, "\\", "/") + return string.find(lower, "disabled", 1, true) ~= nil +end + +local function IsPaperdollBagPlaceholder(tex) + if type(tex) ~= "string" then return false end + local lower = string.lower(tex) + lower = string.gsub(lower, "\\", "/") + + if string.find(lower, "ui-paperdoll-slot-bag", 1, true) then + return true + end + + -- Some clients may return slightly different placeholder paths. + if string.find(lower, "paperdoll", 1, true) and + string.find(lower, "slot", 1, true) and + string.find(lower, "bag", 1, true) then + return true + end + + return false +end + +local function IsUsableBagIconTexture(tex) + if type(tex) ~= "string" or tex == "" then + return false + end + if IsPaperdollBagPlaceholder(tex) then + return false + end + if IsDisabledIconTexture(tex) then + return false + end + return true +end + +local function GetIconFromItemLink(link) + if not link then return nil end + + local _, _, _, _, _, _, _, _, tex = GetItemInfo(link) + if tex then return tex end + + local _, _, itemID = string.find(link, "item:(%d+)") + if itemID then + local _, _, _, _, _, _, _, _, tex2 = GetItemInfo("item:" .. itemID) + if tex2 then return tex2 end + end + + return nil +end + +local function BuildPlayerTradeLinkSet() + local links = {} + if not (TradeFrame and TradeFrame:IsVisible()) then return links end + if not GetTradePlayerItemLink then return links end + for i = 1, 6 do + local link = GetTradePlayerItemLink(i) + if link and link ~= "" then + links[link] = true + end + end + return links +end + +local function CreateCoinDisplay(parent, frameName) + local frame = CreateFrame("Frame", frameName, parent, "SmallMoneyFrameTemplate") + frame:SetFrameStrata(parent:GetFrameStrata()) + frame:SetFrameLevel(parent:GetFrameLevel() + 30) + frame:SetWidth(140) + frame:SetHeight(16) + return frame +end + +local function SetCoinDisplayMoney(display, copper) + if not display then return end + local value = tonumber(copper) or 0 + if value < 0 then value = 0 end + + if SmallMoneyFrame_SetAmount then + local ok = pcall(function() SmallMoneyFrame_SetAmount(display, value) end) + if ok then return end + end + + if MoneyFrame_Update and display.GetName then + local frameName = display:GetName() + if frameName and frameName ~= "" then + pcall(function() MoneyFrame_Update(frameName, value) end) + end + end +end + +-- Create a single item slot button +local function CreateSlot(parent, id) + local button = CreateFrame("Button", "SFramesBagSlot" .. id, parent, "ItemButtonTemplate") + button:RegisterForClicks("LeftButtonUp", "RightButtonUp") + button:RegisterForDrag("LeftButton") + + -- Rounded backdrop style (matching CharacterPanel equipment slots) + local DEFAULT_BORDER = (_A and _A.slotBorder) or { 0.25, 0.25, 0.3, 0.8 } + button:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 18, + insets = { left = 2, right = 2, top = 2, bottom = 2 } + }) + button:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9) + button:SetBackdropBorderColor(DEFAULT_BORDER[1], DEFAULT_BORDER[2], DEFAULT_BORDER[3], DEFAULT_BORDER[4]) + + -- Inset icon within the rounded border + local icon = _G[button:GetName() .. "IconTexture"] + if icon then + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:ClearAllPoints() + icon:SetPoint("TOPLEFT", button, "TOPLEFT", 4, -4) + icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -4, 4) + end + + local qualGlow = button:CreateTexture(nil, "OVERLAY") + qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + qualGlow:SetBlendMode("ADD") + qualGlow:SetAlpha(0.8) + qualGlow:SetWidth(SLOT_SIZE * 1.9) + qualGlow:SetHeight(SLOT_SIZE * 1.9) + qualGlow:SetPoint("CENTER", button, "CENTER", 0, 0) + qualGlow:Hide() + button.qualGlow = qualGlow + + function button:SetBorderColor(r, g, b, a) + self.qualGlow:SetVertexColor(r, g, b) + self.qualGlow:Show() + self._qualityBorder = true + end + function button:ShowBorder() + self._qualityBorder = true + end + function button:HideBorder() + self.qualGlow:Hide() + self._qualityBorder = false + end + + -- Hide the ugly default rounded Blizzard border + local nt = _G[button:GetName() .. "NormalTexture"] + if nt then nt:SetTexture(nil) nt:Hide() end + + -- Grey item marker (a small coin/junk icon in the corner) + local junkIcon = button:CreateTexture(nil, "OVERLAY") + junkIcon:SetTexture("Interface\\Buttons\\UI-GroupLoot-Coin-Up") + junkIcon:SetWidth(14) + junkIcon:SetHeight(14) + -- Place it on top of everything + junkIcon:SetPoint("TOPLEFT", button, "TOPLEFT", 1, -1) + junkIcon:Hide() + button.junkIcon = junkIcon + + local previewGlow = button:CreateTexture(nil, "OVERLAY") + previewGlow:SetTexture("Interface\\Buttons\\ButtonHilight-Square") + previewGlow:SetBlendMode("ADD") + previewGlow:SetAllPoints(button) + previewGlow:Hide() + button.previewGlow = previewGlow + + local tradeText = button:CreateFontString(nil, "OVERLAY") + tradeText:SetFont("Fonts\\Zekton.ttf", 10, "OUTLINE") + if setglobal then -- check Vanilla or use default font + tradeText:SetFontObject(GameFontNormal) + end + tradeText:SetPoint("CENTER", button, "CENTER", 0, 0) + tradeText:SetText("交易") + tradeText:SetTextColor(0, 1, 0, 1) + tradeText:Hide() + button.tradeText = tradeText + + button:SetScript("OnEnter", function() + if this.bagID == nil or this.slotID == nil then return end + SFrames.Bags._hoveredSlot = this + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:ClearLines() + + if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then + local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar) + if data and data.bags[this.bagID] and data.bags[this.bagID].items[this.slotID] then + local link = data.bags[this.bagID].items[this.slotID].link + local shown = false + if link then + local _, _, itemStr = string.find(link, "(item:[%-?%d:]+)") + local ok = false + if itemStr then + ok = pcall(function() GameTooltip:SetHyperlink(itemStr) end) + else + ok = pcall(function() GameTooltip:SetHyperlink(link) end) + end + shown = ok and HasTooltipText() + + if not shown then + local name = GetItemNameFromLink(link) + if name and name ~= "" then + GameTooltip:SetText(name, 1, 1, 1) + shown = true + end + end + + if not shown then + GameTooltip:SetText(TEXT_ITEM, 1, 1, 1) + end + else + GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65) + end + else + GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65) + end + else + local shown = SetTooltipFromContainerItem(this.bagID, this.slotID) + if not shown then + GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65) + end + end + if IsControlKeyDown() then + ShowInspectCursor() + else + ShowMerchantCursorForSlot(this) + end + GameTooltip:Show() + end) + + button:SetScript("OnLeave", function() + SFrames.Bags._hoveredSlot = nil + this.controlDownLast = nil + GameTooltip:Hide() + if HideContainerSellCursor and this.bagID and this.slotID then + pcall(function() + HideContainerSellCursor(this.bagID, this.slotID) + end) + end + ResetCursor() + end) + + local cooldown = CreateFrame("Model", button:GetName().."Cooldown", button, "CooldownFrameTemplate") + cooldown:SetAllPoints(button) + + function button:SplitStack(split) + if not split or split < 1 then return end + if self.bagID == nil or self.slotID == nil then return end + SplitContainerItem(self.bagID, self.slotID, split) + end + + button:SetScript("OnClick", function() + local bagID = this.bagID + local slotID = this.slotID + local isOffline = SFrames.Bags.Container.isOffline + + -- Helper: get item link for this slot (works both online and offline) + local function GetSlotLink() + if isOffline and SFrames.Bags.Container.offlineChar then + local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar) + if data and data.bags[bagID] and data.bags[bagID].items[slotID] then + return data.bags[bagID].items[slotID].link + end + return nil + end + return GetContainerItemLink(bagID, slotID) + end + + if IsControlKeyDown() and arg1 == "LeftButton" then + local link = GetSlotLink() + if link and DressUpItemLink then + DressUpItemLink(link) + return + end + end + if IsShiftKeyDown() then + local eb = ChatFrameEditBox + if eb and eb.IsVisible and eb:IsVisible() then + local link = GetSlotLink() + if link then eb:Insert(link) end + return + end + if not isOffline and arg1 == "LeftButton" and (not CursorHasItem()) and OpenStackSplitFrame then + local _, itemCount = GetContainerItemInfo(bagID, slotID) + if itemCount and itemCount > 1 then + OpenStackSplitFrame(itemCount, this, "BOTTOMLEFT", "TOPLEFT") + return + end + end + end + + -- Block all other actions in offline mode + if isOffline then return end + + if arg1 == "RightButton" then + if SFrames.Mail and SFrames.Mail.TryAddItemFromBag then + if SFrames.Mail.TryAddItemFromBag(bagID, slotID) then return end + end + if this.bagID >= 0 and IsLiveBankOpen() and AutoStoreBagItem then + local ok = pcall(function() AutoStoreBagItem(this.bagID, this.slotID) end) + if ok then return end + end + + if TradeFrame and TradeFrame:IsVisible() and not IsShiftKeyDown() then + local _, _, locked = GetContainerItemInfo(this.bagID, this.slotID) + if locked then + local link = GetSlotLink() + if link and GetTradePlayerItemLink then + local inTradeSlot = nil + for i = 1, 6 do + if GetTradePlayerItemLink(i) == link then + inTradeSlot = i + break + end + end + if inTradeSlot then + ClearCursor() + ClickTradeButton(inTradeSlot) + ClearCursor() + return + end + if BagFrame and BagFrame:IsVisible() then + SFrames.Bags.Container:UpdateLayout() + end + end + else + PickupContainerItem(this.bagID, this.slotID) + local tradeSlot = TradeFrame_GetAvailableSlot and TradeFrame_GetAvailableSlot() + if tradeSlot then ClickTradeButton(tradeSlot) end + if CursorHasItem() then ClearCursor() end + return + end + end + + if AuctionFrame and AuctionFrame:IsShown() and not IsShiftKeyDown() then + if AuctionFrameBrowse and AuctionFrameBrowse:IsShown() then + local link = GetContainerItemLink(this.bagID, this.slotID) + if link then + local _, _, itemName = string.find(link, "%[(.+)%]") + if itemName and BrowseName then + BrowseName:SetText(itemName) + if AuctionFrameBrowse_Search then AuctionFrameBrowse_Search() end + end + end + return + elseif AuctionFrameAuctions and AuctionFrameAuctions:IsShown() then + PickupContainerItem(this.bagID, this.slotID) + if AuctionsItemButton then AuctionsItemButton:Click() end + if CursorHasItem() then ClearCursor() end + return + end + end + + UseContainerItem(this.bagID, this.slotID) + else + PickupContainerItem(this.bagID, this.slotID) + end + end) + + button:SetScript("OnDragStart", function() + if SFrames.Bags.Container.isOffline then return end + if CursorHasItem() then return end + PickupContainerItem(this.bagID, this.slotID) + end) + + button:SetScript("OnReceiveDrag", function() + if SFrames.Bags.Container.isOffline then return end + if CursorHasItem() then + PickupContainerItem(this.bagID, this.slotID) + end + end) + + return button +end + +local function SaveBagFramePosition() + if not (BagFrame and SFramesDB and SFramesDB.Bags) then return end + local point, _, relPoint, x, y = BagFrame:GetPoint() + if not point or not relPoint then return end + SFramesDB.Bags.bagPosition = { + point = point, + relPoint = relPoint, + x = x or 0, + y = y or 0, + } +end + +local function ApplyBagFramePosition() + if not BagFrame then return end + BagFrame:ClearAllPoints() + + local pos = SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bagPosition + if pos and pos.point and pos.relPoint and type(pos.x) == "number" and type(pos.y) == "number" then + BagFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) + else + -- Default to left side; bank frame defaults to right side. + BagFrame:SetPoint("CENTER", UIParent, "CENTER", -360, 0) + end +end + +function SFrames.Bags.Container:PreviewBagSlots(targetBagID) + for _, btn in ipairs(ItemSlots) do + if btn and btn:IsShown() then + local icon = _G[btn:GetName() .. "IconTexture"] + local isMatch = (btn.bagID == targetBagID) + + if icon then + icon:SetVertexColor(1, 1, 1) + end + + if btn.previewGlow then + if isMatch then + btn.previewGlow:Show() + else + btn.previewGlow:Hide() + end + end + end + end +end + +function SFrames.Bags.Container:ClearBagPreview() + for _, btn in ipairs(ItemSlots) do + if btn and btn:IsShown() then + local icon = _G[btn:GetName() .. "IconTexture"] + if icon then icon:SetVertexColor(1, 1, 1) end + if btn.previewGlow then btn.previewGlow:Hide() end + end + end + + -- Restore search dimming state after preview. + local query = "" + if SFramesBagSearchBox and SFramesBagSearchBox.GetText then + query = SFramesBagSearchBox:GetText() or "" + end + if SFrames.Bags.Features and SFrames.Bags.Features.ApplySearch then + SFrames.Bags.Features:ApplySearch(query) + end +end + +-- Build/Update the item slot grid +function SFrames.Bags.Container:UpdateLayout() + if not BagFrame then return end + + local cols = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.columns) or 10 + local spacing = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bagSpacing) or SPACING + spacing = tonumber(spacing) or SPACING + if spacing < 0 then spacing = 0 end + local slots = {} + + local isOffline = self.isOffline + local charName = self.offlineChar + local offlineDB = nil + if isOffline and charName then + offlineDB = SFrames.Bags.Offline:GetCharacterData(charName) + if not offlineDB then + isOffline = false + self.isOffline = false + self.offlineChar = nil + if BagFrame and BagFrame.RefreshCharacterSelectorText then + BagFrame.RefreshCharacterSelectorText() + end + end + end + + local tradeLinks = nil + if not isOffline and TradeFrame and TradeFrame:IsVisible() then + tradeLinks = BuildPlayerTradeLinkSet() + end + + -- Collect all slots (bags 0-4) + for bag = 0, 4 do + local size = 0 + if isOffline and offlineDB then + if offlineDB.bags[bag] then size = offlineDB.bags[bag].size end + else + size = GetContainerNumSlots(bag) + end + for slot = 1, size do + table.insert(slots, { bag = bag, slot = slot }) + end + end + + local numSlots = table.getn(slots) + if numSlots == 0 then numSlots = 1 end + local rows = math.ceil(numSlots / cols) + + -- Resize frame + local width = MARGIN * 2 + (cols * SLOT_SIZE) + math.max(0, (cols - 1)) * spacing + local height = MARGIN * 2 + TOP_OFFSET + (rows * SLOT_SIZE) + math.max(0, (rows - 1)) * spacing + 24 + BagFrame:SetWidth(math.max(width, 160)) + BagFrame:SetHeight(height) + + -- Position & update slots + for i, meta in ipairs(slots) do + local btn = ItemSlots[i] + if not btn then + btn = CreateSlot(BagFrame, i) + ItemSlots[i] = btn + end + + btn.bagID = meta.bag + btn.slotID = meta.slot + + local row = math.floor((i - 1) / cols) + local col = math.mod((i - 1), cols) + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", + MARGIN + col * (SLOT_SIZE + spacing), + -(MARGIN + TOP_OFFSET + row * (SLOT_SIZE + spacing))) + + -- Fetch item info + local texture, count, quality, link, locked + if isOffline and offlineDB then + if offlineDB.bags[meta.bag] and offlineDB.bags[meta.bag].items[meta.slot] then + local item = offlineDB.bags[meta.bag].items[meta.slot] + texture = item.texture + count = item.count + quality = item.quality + link = item.link + locked = false + end + else + local t, c, l, q = GetContainerItemInfo(meta.bag, meta.slot) + texture = t; count = c; locked = l; quality = q + link = GetContainerItemLink(meta.bag, meta.slot) + end + + SetItemButtonTexture(btn, texture) + SetItemButtonCount(btn, count) + local iconTex = _G[btn:GetName() .. "IconTexture"] + + local isDesaturated = locked and (not tradeLinks) + if btn.tradeText then btn.tradeText:Hide() end + if tradeLinks and link then + local isLockedByTrade = locked and (tradeLinks[link] and true or false) + if isLockedByTrade then + if btn.tradeText then btn.tradeText:Show() end + isDesaturated = true + else + -- Ignore stale locked flags for items no longer in trade. + isDesaturated = false + SFramesBagsTooltipScanner = SFramesBagsTooltipScanner or CreateFrame("GameTooltip", "SFramesBagsTooltipScanner", nil, "GameTooltipTemplate") + SFramesBagsTooltipScanner:SetOwner(UIParent, "ANCHOR_NONE") + SFramesBagsTooltipScanner:ClearLines() + local hasItem = SFramesBagsTooltipScanner:SetBagItem(meta.bag, meta.slot) + if hasItem then + for i = 1, 10 do + local line = _G["SFramesBagsTooltipScannerTextLeft" .. i] + if line then + local text = line:GetText() + if text and (text == ITEM_SOULBOUND or text == ITEM_BIND_QUEST or text == "Quest Item") then + isDesaturated = true + break + end + else + break + end + end + end + end + elseif not isOffline and TradeFrame and TradeFrame:IsVisible() and link then + SFramesBagsTooltipScanner = SFramesBagsTooltipScanner or CreateFrame("GameTooltip", "SFramesBagsTooltipScanner", nil, "GameTooltipTemplate") + SFramesBagsTooltipScanner:SetOwner(UIParent, "ANCHOR_NONE") + SFramesBagsTooltipScanner:ClearLines() + local hasItem = SFramesBagsTooltipScanner:SetBagItem(meta.bag, meta.slot) + if hasItem then + for i = 1, 10 do + local line = _G["SFramesBagsTooltipScannerTextLeft" .. i] + if line then + local text = line:GetText() + if text and (text == ITEM_SOULBOUND or text == ITEM_BIND_QUEST or text == "Quest Item") then + isDesaturated = true + break + end + else + break + end + end + end + end + + if iconTex then + if isDesaturated then + iconTex:SetVertexColor(0.5, 0.5, 0.5) + else + iconTex:SetVertexColor(1, 1, 1) + end + end + if btn.previewGlow then btn.previewGlow:Hide() end + + -- Quality border & Grey marker + btn:HideBorder() + btn.junkIcon:Hide() + + if link then + -- 1st attempt: Safest 1.12 generic approach: Read the color straight out of the hyperlink! + -- Standard link format: |cffAABBCC|Hitem:... + local _, _, hex = string.find(link, "|c(%x+)|H") + local parsedColor = false + + if hex and string.len(hex) == 8 then + local hexLower = string.lower(hex) + if hexLower == "ff9d9d9d" then + -- Poor / Grey Item + btn:SetBorderColor(0.5, 0.5, 0.5, 1) + btn:ShowBorder() + btn.junkIcon:Show() + parsedColor = true + elseif hexLower == "ffffffff" then + -- Common / White Item (No special border) + parsedColor = true + else + -- Green, Blue, Purple, Orange + local r = tonumber(string.sub(hex, 3, 4), 16) / 255 + local g = tonumber(string.sub(hex, 5, 6), 16) / 255 + local b = tonumber(string.sub(hex, 7, 8), 16) / 255 + btn:SetBorderColor(r, g, b, 1) + btn:ShowBorder() + parsedColor = true + end + end + + -- 2nd attempt fallback: Use the API quality return values if regex fails + if not parsedColor then + local q = quality + if not q then + local _, _, itemString = string.find(link, "item:(%d+)") + if itemString then + local _, _, scanRarity = GetItemInfo("item:" .. itemString) + q = scanRarity + end + end + + if q then + if q == 0 then + btn:SetBorderColor(0.5, 0.5, 0.5, 1) + btn:ShowBorder() + btn.junkIcon:Show() + elseif q > 1 then + local r, g, b = GetItemQualityColor(q) + btn:SetBorderColor(r, g, b, 1) + btn:ShowBorder() + end + end + end + end + + -- Cooldowns + local cooldown = _G[btn:GetName() .. "Cooldown"] + if cooldown then + if isOffline then + cooldown:Hide() + else + local start, duration, enable = GetContainerItemCooldown(meta.bag, meta.slot) + if start and duration and start > 0 and duration > 0 then + CooldownFrame_SetTimer(cooldown, start, duration, enable) + cooldown:Show() + else + cooldown:Hide() + end + end + end + + btn:Show() + end + + -- Hide excess buttons + for i = numSlots + 1, table.getn(ItemSlots) do + if ItemSlots[i] then ItemSlots[i]:Hide() end + end + + if BagFrame.UpdateBagSlotIcons then + BagFrame.UpdateBagSlotIcons() + end + + -- Update money display + if BagFrame.moneyFrame then + local copper = 0 + if isOffline and offlineDB then + copper = offlineDB.money or 0 + else + copper = GetMoney() + end + SetCoinDisplayMoney(BagFrame.moneyFrame, copper) + end +end + +-- Main frame initialization (called once after PLAYER_LOGIN) +function SFrames.Bags.Container:Initialize() + if BagFrame then return end + + BagFrame = CreateFrame("Frame", "SFramesBagFrame", UIParent) + BagFrame:SetWidth(420) + BagFrame:SetHeight(200) + BagFrame:SetFrameStrata("HIGH") + BagFrame:SetToplevel(true) + BagFrame:EnableMouse(true) + BagFrame:SetMovable(true) + BagFrame:SetClampedToScreen(true) + BagFrame:RegisterForDrag("LeftButton") + BagFrame:SetScript("OnDragStart", function() this:StartMoving() end) + BagFrame:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + SaveBagFramePosition() + end) + ApplyBagFramePosition() + tinsert(UISpecialFrames, "SFramesBagFrame") + + BagFrame:SetScript("OnUpdate", function() + local btn = SFrames.Bags._hoveredSlot + if not btn then return end + if not GameTooltip:IsOwned(btn) then + SFrames.Bags._hoveredSlot = nil + return + end + if IsControlKeyDown() then + if not btn.controlDownLast then + btn.controlDownLast = true + ShowInspectCursor() + end + else + if btn.controlDownLast then + btn.controlDownLast = false + ResetCursor() + ShowMerchantCursorForSlot(btn) + end + end + end) + + -- ESC menu style rounded backdrop + BagFrame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + local _A = SFrames.ActiveTheme + if _A and _A.panelBg then + BagFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4] or 0.95) + BagFrame:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4] or 0.9) + else + BagFrame:SetBackdropColor(0.12, 0.06, 0.10, 0.95) + BagFrame:SetBackdropBorderColor(0.55, 0.30, 0.42, 0.9) + end + local bagShadow = CreateFrame("Frame", nil, BagFrame) + bagShadow:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", -5, 5) + bagShadow:SetPoint("BOTTOMRIGHT", BagFrame, "BOTTOMRIGHT", 5, -5) + bagShadow:SetFrameLevel(math.max(BagFrame:GetFrameLevel() - 1, 0)) + bagShadow:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + bagShadow:SetBackdropColor(0, 0, 0, 0.55) + bagShadow:SetBackdropBorderColor(0, 0, 0, 0.4) + local scale = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.scale) == "number" and SFramesDB.Bags.scale) or 0.85 + BagFrame:SetScale(scale) + + local titleIco = SFrames:CreateIcon(BagFrame, "backpack", 14) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", 10, -7) + titleIco:SetVertexColor(_A.title[1], _A.title[2], _A.title[3]) + + local titleFS = SFrames:CreateFontString(BagFrame, 12, "LEFT") + titleFS:SetPoint("LEFT", titleIco, "RIGHT", 4, 0) + titleFS:SetText(TEXT_BAGS_TITLE) + titleFS:SetTextColor(_A.title[1], _A.title[2], _A.title[3]) + BagFrame.title = titleFS + + -- Close button + local closeBtn = CreateFrame("Button", "SFramesBagClose", BagFrame, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", BagFrame, "TOPRIGHT", 0, 0) + closeBtn:SetScript("OnClick", function() SFrames.Bags.Container:Close() end) + + -- Money display + BagFrame.moneyFrame = CreateCoinDisplay(BagFrame, "SFramesBagMoneyFrame") + BagFrame.moneyFrame:SetPoint("RIGHT", BagFrame, "BOTTOMRIGHT", -8, 17) + SetCoinDisplayMoney(BagFrame.moneyFrame, 0) + + -- Bag slot management buttons (5 slots: 0=backpack, 1-4 = equipped bags) + BagFrame.bagSlotBtns = {} + local BAG_BTN_SIZE = 22 + ResolvePlayerBagInvSlots() + local bagInvSlots = playerBagInvSlots + local bagBar = CreateFrame("Frame", "SFramesBagBar", BagFrame) + bagBar:SetWidth((BAG_BTN_SIZE * 5) + (3 * 4)) + bagBar:SetHeight(BAG_BTN_SIZE) + bagBar:SetPoint("BOTTOMLEFT", BagFrame, "BOTTOMLEFT", 8, 6) + bagBar:SetFrameStrata(BagFrame:GetFrameStrata()) + bagBar:SetFrameLevel(BagFrame:GetFrameLevel() + 40) + bagBar:EnableMouse(false) + BagFrame.bagBar = bagBar + + local function PlaceCursorItemInBagSlot(invSlot) + if not invSlot or not CursorHasItem() then return false end + + local ok = false + if EquipCursorItem then + ok = pcall(function() EquipCursorItem(invSlot) end) + if ok and (not CursorHasItem()) then + return true + end + end + + -- In vanilla this can both place and swap bag items in bag slots. + ok = pcall(function() PickupBagFromSlot(invSlot) end) + if ok and (not CursorHasItem()) then + return true + end + + if PutItemInBag then + ok = pcall(function() PutItemInBag(invSlot) end) + if ok and (not CursorHasItem()) then + return true + end + end + + -- Fallback path if PutItemInBag is unavailable. + if CursorHasItem() then + local fallbackOk = pcall(function() PickupInventoryItem(invSlot) end) + ok = fallbackOk or ok + end + + return ok and (not CursorHasItem()) + end + + for bagIndex = 0, 4 do + local bsBtn = CreateFrame("Button", "SFramesBagMgrBtn"..bagIndex, bagBar) + bsBtn:SetWidth(BAG_BTN_SIZE) + bsBtn:SetHeight(BAG_BTN_SIZE) + bsBtn:EnableMouse(true) + bsBtn:SetFrameStrata(bagBar:GetFrameStrata()) + bsBtn:SetFrameLevel(bagBar:GetFrameLevel() + 1) + + -- Anchor: first one at BOTTOMLEFT, rest chained RIGHT + if bagIndex == 0 then + bsBtn:SetPoint("TOPLEFT", bagBar, "TOPLEFT", 0, 0) + else + bsBtn:SetPoint("LEFT", _G["SFramesBagMgrBtn" .. (bagIndex-1)], "RIGHT", 3, 0) + end + bsBtn.bagID = bagIndex + + -- Rounded backdrop (matching item slot style) + bsBtn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 } + }) + bsBtn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9) + bsBtn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8) + + -- Icon texture inset within border + local bsIcon = bsBtn:CreateTexture(nil, "OVERLAY") + bsIcon:SetPoint("TOPLEFT", bsBtn, "TOPLEFT", 2, -2) + bsIcon:SetPoint("BOTTOMRIGHT", bsBtn, "BOTTOMRIGHT", -2, 2) + bsIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + bsIcon:SetBlendMode("BLEND") + bsIcon:SetVertexColor(1, 1, 1, 1) + bsBtn.icon = bsIcon + + local bsHighlight = bsBtn:CreateTexture(nil, "HIGHLIGHT") + bsHighlight:SetTexture("Interface\\Buttons\\ButtonHilight-Square") + bsHighlight:SetBlendMode("ADD") + bsHighlight:SetAllPoints(bsBtn) + + bsBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:ClearLines() + if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then + local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar) + if this.bagID == 0 then + local slots = data and data.bags and data.bags[0] and data.bags[0].size or 0 + if slots and slots > 0 then + GameTooltip:SetText(string.format("%s (%d%s)", TEXT_BACKPACK, slots, TEXT_BAG_SLOTS), 1, 1, 1) + else + GameTooltip:SetText(TEXT_BACKPACK, 1, 1, 1) + end + else + local shown = false + local slots = data and data.bags and data.bags[this.bagID] and data.bags[this.bagID].size or 0 + local bagMeta = data and data.equippedBags and data.equippedBags[this.bagID] + + if bagMeta and bagMeta.link then + local ok = pcall(function() GameTooltip:SetHyperlink(bagMeta.link) end) + shown = ok and HasTooltipText() + + if not shown then + local name = GetItemNameFromLink(bagMeta.link) + if name and name ~= "" then + GameTooltip:SetText(name, 1, 1, 1) + shown = true + end + end + end + + if not shown then + if slots and slots > 0 then + GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BAG_SLOT, this.bagID, slots, TEXT_BAG_SLOTS), 1, 1, 1) + else + GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BAG_SLOT, this.bagID, TEXT_BAG_EMPTY), 0.6, 0.6, 0.6) + end + end + end + GameTooltip:AddLine(TEXT_OFFLINE, 0.75, 0.75, 0.75) + else + if this.bagID == 0 then + GameTooltip:SetText(TEXT_BACKPACK, 1, 1, 1) + else + local invSlot = playerBagInvSlots[this.bagID] + local shown = false + if invSlot then + if GameTooltip.SetInventoryItem then + local ok = pcall(function() GameTooltip:SetInventoryItem("player", invSlot) end) + if ok then + local left1 = _G["GameTooltipTextLeft1"] + if left1 and left1:GetText() and left1:GetText() ~= "" then + shown = true + end + end + end + + if not shown then + local bagLink = GetInventoryItemLink("player", invSlot) + if bagLink then + local ok = pcall(function() GameTooltip:SetHyperlink(bagLink) end) + if ok then + shown = true + else + local name = GetItemInfo(bagLink) or (TEXT_EQUIPPED_BAG.." "..this.bagID) + GameTooltip:SetText(name, 1, 1, 1) + shown = true + end + end + end + + if not shown then + local slots = GetContainerNumSlots(this.bagID) or 0 + if slots > 0 then + GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BAG_SLOT, this.bagID, slots, TEXT_BAG_SLOTS), 1, 1, 1) + else + GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BAG_SLOT, this.bagID, TEXT_BAG_EMPTY), 0.6, 0.6, 0.6) + end + end + else + GameTooltip:SetText(string.format("%s %d", TEXT_BAG_SLOT, this.bagID), 0.8, 0.8, 0.8) + end + end + end + if BagFrame:IsVisible() then + SFrames.Bags.Container:PreviewBagSlots(this.bagID) + end + GameTooltip:Show() + end) + bsBtn:SetScript("OnLeave", function() + GameTooltip:Hide() + if BagFrame:IsVisible() then + SFrames.Bags.Container:ClearBagPreview() + end + end) + + bsBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp") + bsBtn:RegisterForDrag("LeftButton") + + bsBtn:SetScript("OnDragStart", function() + if this.bagID <= 0 then return end + local invSlot = playerBagInvSlots[this.bagID] + if invSlot then + pcall(function() PickupBagFromSlot(invSlot) end) + end + end) + + bsBtn:SetScript("OnReceiveDrag", function() + if not CursorHasItem() then return end + + if this.bagID == 0 then + if PutItemInBackpack then + pcall(function() PutItemInBackpack() end) + end + else + local invSlot = playerBagInvSlots[this.bagID] + PlaceCursorItemInBagSlot(invSlot) + end + + if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end + if BagFrame:IsVisible() then SFrames.Bags.Container:UpdateLayout() end + end) + + bsBtn:SetScript("OnClick", function() + if CursorHasItem() then + if this.bagID == 0 then + if PutItemInBackpack then + pcall(function() PutItemInBackpack() end) + end + else + local invSlot = playerBagInvSlots[this.bagID] + PlaceCursorItemInBagSlot(invSlot) + end + if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end + if BagFrame:IsVisible() then SFrames.Bags.Container:UpdateLayout() end + return + end + + if this.bagID > 0 then + local invSlot = playerBagInvSlots[this.bagID] + if invSlot then + pcall(function() PickupBagFromSlot(invSlot) end) + end + elseif arg1 == "RightButton" then + -- Backpack slot has no equip slot to pick up from. + else + SFrames.Bags.Container:Toggle() + end + end) + + BagFrame.bagSlotBtns[bagIndex] = bsBtn + end + -- Update bag slot textures whenever bag contents change + BagFrame.UpdateBagSlotIcons = function() + local isOffline = SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar + local offlineDB = nil + if isOffline then + offlineDB = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar) + end + + ResolvePlayerBagInvSlots() + for bagIndex = 0, 4 do + local btn = BagFrame.bagSlotBtns[bagIndex] + if btn then + if bagIndex == 0 then + btn.icon:SetTexture("Interface\\Buttons\\Button-Backpack-Up") + elseif isOffline and offlineDB then + local slots = offlineDB.bags and offlineDB.bags[bagIndex] and offlineDB.bags[bagIndex].size or 0 + local bagMeta = offlineDB.equippedBags and offlineDB.equippedBags[bagIndex] + local tex = nil + + if bagMeta and bagMeta.link then + tex = GetIconFromItemLink(bagMeta.link) + end + if (not tex) and bagMeta and IsUsableBagIconTexture(bagMeta.texture) then + tex = bagMeta.texture + end + + if (not tex) and slots and slots > 0 then + tex = "Interface\\Icons\\INV_Misc_Bag_08" + end + if not tex then + tex = "Interface\\Icons\\INV_Misc_Bag_08" + end + btn.icon:SetTexture(tex) + else + local invSlot = playerBagInvSlots[bagIndex] + local tex = nil + local slots = GetContainerNumSlots(bagIndex) or 0 + + if invSlot then + local link = GetInventoryItemLink("player", invSlot) + tex = GetIconFromItemLink(link) + + if (not tex) and slots > 0 then + local rawTex = GetInventoryItemTexture("player", invSlot) + if type(rawTex) ~= "string" then + tex = rawTex + elseif IsUsableBagIconTexture(rawTex) then + tex = rawTex + end + end + end + + if (not tex) and slots > 0 then + local liveTex = GetLivePlayerBagIconTexture(bagIndex) + if IsUsableBagIconTexture(liveTex) then + tex = liveTex + end + end + + if tex then + btn.icon:SetTexture(tex) + else + btn.icon:SetTexture("Interface\\Icons\\INV_Misc_Bag_08") + end + end + btn:Enable() + if btn.icon.SetDesaturated then + pcall(function() btn.icon:SetDesaturated(false) end) + end + btn.icon:SetVertexColor(1, 1, 1, 1) + btn.icon:SetAlpha(1) + end + end + end + local function QueueBagSlotIconRefreshes() + if not BagFrame or not BagFrame.UpdateBagSlotIcons then return end + local elapsed = 0 + local nextRefresh = 1 + local refreshPoints = { 0.08, 0.25, 0.55 } + local refreshTimer = CreateFrame("Frame") + refreshTimer:SetScript("OnUpdate", function() + elapsed = elapsed + arg1 + if nextRefresh <= table.getn(refreshPoints) and elapsed >= refreshPoints[nextRefresh] then + if BagFrame.UpdateBagSlotIcons then + BagFrame.UpdateBagSlotIcons() + end + if BagFrame:IsVisible() then + SFrames.Bags.Container:UpdateLayout() + end + nextRefresh = nextRefresh + 1 + end + if nextRefresh > table.getn(refreshPoints) then + this:SetScript("OnUpdate", nil) + end + end) + end + BagFrame.UpdateBagSlotIcons() + QueueBagSlotIconRefreshes() + + -- Search bar + local eb = CreateFrame("EditBox", "SFramesBagSearchBox", BagFrame, "InputBoxTemplate") + eb:SetWidth(120) + eb:SetHeight(18) + eb:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", 10, -26) + eb:SetAutoFocus(false) + eb:SetScript("OnEnterPressed", function() this:ClearFocus() end) + eb:SetScript("OnEscapePressed", function() this:ClearFocus(); this:SetText("") end) + eb:SetScript("OnTextChanged", function() + SFrames.Bags.Features:ApplySearch(this:GetText()) + end) + + local function CreateHeaderIconButton(name, parent, iconPath) + local btn = CreateFrame("Button", name, parent) + btn:SetWidth(18) + btn:SetHeight(18) + btn:SetFrameStrata(parent:GetFrameStrata()) + btn:SetFrameLevel(parent:GetFrameLevel() + 45) + + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 } + }) + btn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.86) + btn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8) + + local icon = btn:CreateTexture(nil, "ARTWORK") + icon:SetTexture(iconPath or "Interface\\Icons\\INV_Misc_QuestionMark") + icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2) + icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + btn.icon = icon + + local hl = btn:CreateTexture(nil, "HIGHLIGHT") + hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square") + hl:SetBlendMode("ADD") + hl:SetAllPoints(btn) + + return btn + end + + -- Character selector: icon button-triggered dropdown. + local function GetCurrentCharacterName() + local live = UnitName("player") + if type(live) == "string" and live ~= "" then + return live + end + local cached = SFrames.Bags.Offline:GetCurrentPlayerName() + if type(cached) == "string" and cached ~= "" then + return cached + end + return TEXT_CHARACTER + end + + local function GetOfflineBankTargetCharacter() + if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then + return SFrames.Bags.Container.offlineChar + end + return GetCurrentCharacterName() + end + + local charBtn = CreateHeaderIconButton("SFramesBagCharBtn", BagFrame, CHARACTER_SELECTOR_ICON) + charBtn:SetPoint("TOPRIGHT", BagFrame, "TOPRIGHT", -8, -26) + BagFrame.charSelectBtn = charBtn + + local cfgBtn = CreateHeaderIconButton("SFramesBagConfigBtn", BagFrame, "Interface\\Icons\\INV_Misc_Gear_01") + cfgBtn:SetPoint("RIGHT", charBtn, "LEFT", -4, 0) + SFrames:SetIcon(cfgBtn.icon, "settings") + cfgBtn:SetScript("OnClick", function() + if SFrames.ConfigUI and SFrames.ConfigUI.Build then + SFrames.ConfigUI:Build("bags") + end + end) + cfgBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(TEXT_SETTINGS, 1, 1, 1) + GameTooltip:Show() + end) + cfgBtn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + local bankBtn = CreateHeaderIconButton("SFramesBagOfflineBankBtn", BagFrame, "Interface\\Icons\\INV_Misc_Key_05") + bankBtn:SetPoint("RIGHT", cfgBtn, "LEFT", -4, 0) + SFrames:SetIcon(bankBtn.icon, "gold") + bankBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(TEXT_OFFLINE .. TEXT_BANK_TITLE, 1, 1, 1) + local target = GetOfflineBankTargetCharacter() + if target and target ~= "" and target ~= TEXT_CHARACTER then + GameTooltip:AddLine(target, 0.8, 0.8, 0.8) + end + GameTooltip:Show() + end) + bankBtn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + bankBtn:SetScript("OnClick", function() + if not (SFrames.Bags and SFrames.Bags.Bank) then return end + local target = GetOfflineBankTargetCharacter() + if target == TEXT_CHARACTER then + target = nil + end + if SFrames.Bags.Bank.OpenOffline then + SFrames.Bags.Bank:OpenOffline(target) + else + SFrames.Bags.Bank:Open() + end + end) + + -- Sort button (icon) + local sortBtn = CreateHeaderIconButton("SFramesBagSortBtn", BagFrame, "Interface\\Icons\\INV_Misc_Note_05") + sortBtn:SetPoint("LEFT", eb, "RIGHT", 6, 0) + SFrames:SetIcon(sortBtn.icon, "backpack") + sortBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(TEXT_SORT, 1, 1, 1) + GameTooltip:Show() + end) + sortBtn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + sortBtn:SetScript("OnClick", function() + if SFrames.Bags.Sort then SFrames.Bags.Sort:Start() end + end) + + local hsBtn = CreateHeaderIconButton("SFramesBagHSBtn", BagFrame, "Interface\\Icons\\INV_Misc_Rune_01") + hsBtn:SetPoint("LEFT", sortBtn, "RIGHT", 4, 0) + SFrames:SetIcon(hsBtn.icon, "hearthstone") + hsBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(TEXT_HS_USE, 1, 1, 1) + GameTooltip:Show() + end) + hsBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) + + hsBtn:SetScript("OnClick", function() + for bag = 0, 4 do + for slot = 1, GetContainerNumSlots(bag) do + local link = GetContainerItemLink(bag, slot) + if link and (string.find(link, "6948") or string.find(link, "Hearthstone")) then + UseContainerItem(bag, slot) + return + end + end + end + SFrames:Print(TEXT_HS_NOT_FOUND) + end) + + local keyBtn = CreateHeaderIconButton("SFramesBagKeyBtn", BagFrame, "Interface\\Icons\\INV_Misc_Key_04") + keyBtn:SetPoint("LEFT", hsBtn, "RIGHT", 4, 0) + SFrames:SetIcon(keyBtn.icon, "key") + keyBtn:RegisterForClicks("LeftButtonUp") + keyBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText("\233\146\165\229\140\153\233\147\190", 1, 0.82, 0) + GameTooltip:Show() + end) + keyBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) + keyBtn:SetScript("OnClick", function() + if SFrames.Bags._origToggleKeyRing then + SFrames.Bags._origToggleKeyRing() + elseif ToggleKeyRing then + ToggleKeyRing() + end + end) + + local dbMenu = CreateFrame("Frame", "SFramesBagDropdown", BagFrame, "UIDropDownMenuTemplate") + dbMenu:Hide() + dbMenu:SetPoint("TOPRIGHT", charBtn, "BOTTOMRIGHT", 0, 0) + + local function RefreshCharacterSelectorText() + if not BagFrame or not BagFrame.charSelectBtn then return end + if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then + BagFrame.charSelectorLabel = SFrames.Bags.Container.offlineChar .. " (" .. TEXT_OFFLINE .. ")" + else + BagFrame.charSelectorLabel = GetCurrentCharacterName() .. " (" .. TEXT_ONLINE .. ")" + end + end + BagFrame.RefreshCharacterSelectorText = RefreshCharacterSelectorText + + local function OnDropdownClick() + local char = this.value + if char == "ONLINE" then + SFrames.Bags.Container.isOffline = false + SFrames.Bags.Container.offlineChar = nil + else + SFrames.Bags.Container.isOffline = true + SFrames.Bags.Container.offlineChar = char + end + RefreshCharacterSelectorText() + SFrames.Bags.Container:UpdateLayout() + end + + UIDropDownMenu_Initialize(dbMenu, function() + local info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {} + local currentName = GetCurrentCharacterName() + + info.text = currentName .. " (" .. TEXT_ONLINE .. ")" + info.value = "ONLINE" + info.func = OnDropdownClick + info.checked = (not SFrames.Bags.Container.isOffline) + UIDropDownMenu_AddButton(info) + + local chars = SFrames.Bags.Offline:GetCharacterList() + table.sort(chars) + for _, char in ipairs(chars) do + if char ~= currentName then + info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {} + info.text = char .. " (" .. TEXT_OFFLINE .. ")" + info.value = char + info.func = OnDropdownClick + info.checked = SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar == char + UIDropDownMenu_AddButton(info) + end + end + end) + + charBtn:SetScript("OnClick", function() + ToggleDropDownMenu(1, nil, dbMenu, this, 0, 0) + end) + charBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(BagFrame.charSelectorLabel or TEXT_CHARACTER, 1, 1, 1) + GameTooltip:Show() + end) + charBtn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + RefreshCharacterSelectorText() + + -- React to bag updates while open + BagFrame:RegisterEvent("BAG_UPDATE") + BagFrame:RegisterEvent("PLAYER_MONEY") + BagFrame:RegisterEvent("BAG_UPDATE_COOLDOWN") + BagFrame:RegisterEvent("UNIT_INVENTORY_CHANGED") + BagFrame:RegisterEvent("PLAYER_ENTERING_WORLD") + BagFrame:RegisterEvent("TRADE_SHOW") + BagFrame:RegisterEvent("TRADE_CLOSED") + BagFrame:RegisterEvent("TRADE_UPDATE") + BagFrame:RegisterEvent("TRADE_PLAYER_ITEM_CHANGED") + BagFrame:RegisterEvent("TRADE_TARGET_ITEM_CHANGED") + BagFrame:RegisterEvent("TRADE_ACCEPT_UPDATE") + BagFrame:SetScript("OnEvent", function() + if BagFrame:IsVisible() then + SFrames.Bags.Container:UpdateLayout() + end + if event == "PLAYER_ENTERING_WORLD" then + if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end + QueueBagSlotIconRefreshes() + elseif event == "BAG_UPDATE" or event == "UNIT_INVENTORY_CHANGED" then + if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end + elseif event == "TRADE_SHOW" or event == "TRADE_CLOSED" or event == "TRADE_UPDATE" + or event == "TRADE_PLAYER_ITEM_CHANGED" or event == "TRADE_TARGET_ITEM_CHANGED" + or event == "TRADE_ACCEPT_UPDATE" then + if event == "TRADE_CLOSED" then + for _, btn in ipairs(ItemSlots) do + if btn.tradeText then btn.tradeText:Hide() end + local iconTex = _G[btn:GetName() .. "IconTexture"] + if iconTex and iconTex.SetDesaturated then + pcall(function() iconTex:SetDesaturated(false) end) + end + if iconTex then iconTex:SetVertexColor(1, 1, 1, 1) end + end + end + if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end + QueueBagSlotIconRefreshes() + end + end) + + BagFrame:Hide() +end + +function SFrames.Bags.Container:Toggle() + if not BagFrame then return end + if BagFrame:IsVisible() then self:Close() else self:Open() end +end + +function SFrames.Bags.Container:Open() + if not BagFrame then return end + self.isOffline = false + self.offlineChar = nil + if BagFrame.RefreshCharacterSelectorText then BagFrame.RefreshCharacterSelectorText() end + -- Clear search + if SFramesBagSearchBox then SFramesBagSearchBox:SetText("") end + self:UpdateLayout() + BagFrame:Show() + local elapsed = 0 + local nextRefresh = 1 + local refreshPoints = { 0.05, 0.18, 0.40 } + local refreshTimer = CreateFrame("Frame") + refreshTimer:SetScript("OnUpdate", function() + elapsed = elapsed + arg1 + if nextRefresh <= table.getn(refreshPoints) and elapsed >= refreshPoints[nextRefresh] then + if BagFrame:IsVisible() then + if BagFrame.UpdateBagSlotIcons then + BagFrame.UpdateBagSlotIcons() + end + SFrames.Bags.Container:UpdateLayout() + end + nextRefresh = nextRefresh + 1 + end + if nextRefresh > table.getn(refreshPoints) then + this:SetScript("OnUpdate", nil) + end + end) + PlaySound("igBackPackOpen") +end + +function SFrames.Bags.Container:Close() + if not BagFrame then return end + BagFrame:Hide() + PlaySound("igBackPackClose") +end diff --git a/Bags/Core.lua b/Bags/Core.lua new file mode 100644 index 0000000..c6c91d4 --- /dev/null +++ b/Bags/Core.lua @@ -0,0 +1,113 @@ +-------------------------------------------------------------------------------- +-- S-Frames: Bag Module Core (Bags/Core.lua) +-- Entry point, registered on PLAYER_LOGIN +-------------------------------------------------------------------------------- + +SFrames.Bags.Core = {} + +-- Ensure default config exists +if not SFrames.Config.Bags then + SFrames.Config.Bags = { + enable = true, + columns = 10, + bagSpacing = 0, + scale = 1, + sellGrey = true, + bankColumns = 12, + bankSpacing = 0, + bankScale = 1, + } +end + +local function EnsureDB() + if not SFramesDB then SFramesDB = {} end + if not SFramesDB.Bags then + SFramesDB.Bags = {} + for k, v in pairs(SFrames.Config.Bags) do + SFramesDB.Bags[k] = v + end + end + -- Fill missing keys with defaults + for k, v in pairs(SFrames.Config.Bags) do + if SFramesDB.Bags[k] == nil then + SFramesDB.Bags[k] = v + end + end +end + +local function HookBagFunctions() + -- Save original functions + local _OpenAllBags = OpenAllBags + local _CloseAllBags = CloseAllBags + local _ToggleBag = ToggleBag + local _ToggleBackpack = ToggleBackpack + local _ToggleKeyRing = ToggleKeyRing + SFrames.Bags._origToggleKeyRing = _ToggleKeyRing + + OpenAllBags = function() + if SFramesDB.Bags.enable then + SFrames.Bags.Container:Open() + else + _OpenAllBags() + end + end + + CloseAllBags = function() + if SFramesDB.Bags.enable then + SFrames.Bags.Container:Close() + else + _CloseAllBags() + end + end + + ToggleBag = function(id) + if id == -2 or id == (KEYRING_CONTAINER or -2) then + _ToggleBag(id) + return + end + if SFramesDB.Bags.enable then + SFrames.Bags.Container:Toggle() + else + _ToggleBag(id) + end + end + + ToggleBackpack = function() + if SFramesDB.Bags.enable then + SFrames.Bags.Container:Toggle() + else + _ToggleBackpack() + end + end + + -- Keyring: always use original keyring window + ToggleKeyRing = function() + _ToggleKeyRing() + end +end + +function SFrames.Bags.Core:Initialize() + SFrames:Print("Debug: Bags Core Initializing...") + EnsureDB() + if not SFramesDB.Bags.enable then + SFrames:Print("Debug: Bags are disabled in config.") + return + end + + SFrames:Print("Debug: Hooking functions...") + HookBagFunctions() + + SFrames:Print("Debug: Init Container...") + SFrames.Bags.Container:Initialize() + + SFrames:Print("Debug: Init Bank...") + if SFrames.Bags.Bank then SFrames.Bags.Bank:Initialize() end + + SFrames:Print("Debug: Init Features...") + SFrames.Bags.Features:Initialize() + + SFrames:Print("Debug: Bags Init Complete.") +end + +-- NOTE: Initialize is called from Core.lua SFrames:Initialize() on PLAYER_LOGIN. +-- Do NOT register PLAYER_LOGIN here to avoid double initialization. diff --git a/Bags/Features.lua b/Bags/Features.lua new file mode 100644 index 0000000..a711e14 --- /dev/null +++ b/Bags/Features.lua @@ -0,0 +1,139 @@ +-------------------------------------------------------------------------------- +-- S-Frames: Bag Module Features (Bags/Features.lua) +-- Auto-sell grey + search apply (buttons are now built in Container.lua) +-------------------------------------------------------------------------------- + +SFrames.Bags.Features = {} + +function SFrames.Bags.Features:Initialize() + self:SetupAutoSell() + self:SetupAutoOpenBags() +end + +-- Auto Sell Grey Items when merchant opens +function SFrames.Bags.Features:SetupAutoSell() + local f = CreateFrame("Frame") + f:RegisterEvent("MERCHANT_SHOW") + f:SetScript("OnEvent", function() + if not (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.sellGrey) then return end + for bag = 0, 4 do + for slot = 1, GetContainerNumSlots(bag) do + local link = GetContainerItemLink(bag, slot) + if link then + -- Extract item ID from link (format: item:XXXX:...) + local _, _, itemString = string.find(link, "item:(%d+)") + if itemString then + local _, _, _, _, _, _, _, _, _, _, itemSellPrice = GetItemInfo("item:" .. itemString) + -- GetItemInfo returns quality as 5th return + local _, _, itemRarity = GetItemInfo("item:" .. itemString) + -- safer: use a scan tooltip to get quality + local scanName, _, scanRarity = GetItemInfo("item:" .. itemString) + if scanRarity and scanRarity == 0 then + local _, itemCount = GetContainerItemInfo(bag, slot) + UseContainerItem(bag, slot) + if scanName then + SFrames:Print("售出: " .. scanName .. " x" .. (itemCount or 1)) + end + end + end + end + end + end + end) +end + +-- Auto open/close bags at Bank, Merchant, Mail, AH, Trade +function SFrames.Bags.Features:SetupAutoOpenBags() + local f = CreateFrame("Frame") + f:RegisterEvent("MERCHANT_SHOW") + f:RegisterEvent("BANKFRAME_OPENED") + f:RegisterEvent("MAIL_SHOW") + f:RegisterEvent("AUCTION_HOUSE_SHOW") + f:RegisterEvent("TRADE_SHOW") + + f:RegisterEvent("MERCHANT_CLOSED") + f:RegisterEvent("BANKFRAME_CLOSED") + f:RegisterEvent("MAIL_CLOSED") + f:RegisterEvent("AUCTION_HOUSE_CLOSED") + f:RegisterEvent("TRADE_CLOSED") + + local autoOpened = false + + f:SetScript("OnEvent", function() + if not (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.enable) then return end + + -- Shows + if event == "MERCHANT_SHOW" or event == "BANKFRAME_OPENED" or event == "MAIL_SHOW" or event == "AUCTION_HOUSE_SHOW" or event == "TRADE_SHOW" then + if SFramesBagFrame and not SFramesBagFrame:IsVisible() then + autoOpened = true + SFrames.Bags.Container:Open() + end + -- Closes + elseif event == "MERCHANT_CLOSED" or event == "BANKFRAME_CLOSED" or event == "MAIL_CLOSED" or event == "AUCTION_HOUSE_CLOSED" or event == "TRADE_CLOSED" then + if autoOpened then + SFrames.Bags.Container:Close() + autoOpened = false + end + end + end) +end + +-- Search filter - called from the search EditBox OnTextChanged +function SFrames.Bags.Features:ApplySearch(query) + -- In WoW 1.12 Chinese client, text is GBK encoded. + -- using string.lower on GBK strings corrupts Chinese characters because the 2nd byte + -- of Chinese characters can hit the ASCII uppercase range (A-Z). + local safeQuery = query or "" + safeQuery = string.gsub(safeQuery, "^%s+", "") + safeQuery = string.gsub(safeQuery, "%s+$", "") + if not string.find(safeQuery, "%S") then + safeQuery = "" + end + + local function ProcessSlots(prefix) + for i = 1, 250 do + local btn = _G[prefix .. i] + if not btn then break end + + local icon = _G[btn:GetName() .. "IconTexture"] + + if btn:IsShown() and icon then + if safeQuery == "" then + icon:SetVertexColor(1, 1, 1) + if btn.border then btn.border:SetAlpha(1) end + if btn.junkIcon then btn.junkIcon:SetAlpha(1) end + if btn.qualGlow then btn.qualGlow:SetAlpha(0.8) end + elseif btn.bagID ~= nil and btn.slotID ~= nil then + local link = GetContainerItemLink(btn.bagID, btn.slotID) + if link then + local name = GetItemInfo(link) + if not name then + local _, _, parsedName = string.find(link, "%[(.+)%]") + name = parsedName + end + + -- Exact substring match (safe for GBK) + if name and string.find(name, safeQuery, 1, true) then + icon:SetVertexColor(1, 1, 1) + if btn.border then btn.border:SetAlpha(1) end + if btn.junkIcon then btn.junkIcon:SetAlpha(1) end + if btn.qualGlow then btn.qualGlow:SetAlpha(0.8) end + else + icon:SetVertexColor(0.45, 0.45, 0.45) + if btn.border then btn.border:SetAlpha(0.4) end + if btn.junkIcon then btn.junkIcon:SetAlpha(0.4) end + if btn.qualGlow then btn.qualGlow:SetAlpha(0.15) end + end + else + icon:SetVertexColor(0.45, 0.45, 0.45) + if btn.border then btn.border:SetAlpha(0.4) end + if btn.junkIcon then btn.junkIcon:SetAlpha(0.4) end + if btn.qualGlow then btn.qualGlow:SetAlpha(0.15) end + end + end + end + end + end + + ProcessSlots("SFramesBagSlot") +end diff --git a/Bags/Offline.lua b/Bags/Offline.lua new file mode 100644 index 0000000..7ae6c7a --- /dev/null +++ b/Bags/Offline.lua @@ -0,0 +1,337 @@ +-------------------------------------------------------------------------------- +-- S-Frames: Offline DB for Bag Module (Bags/Offline.lua) +-- Tracks inventory across characters and realms +-- NOTE: This file defines SFrames.Bags first - must be loaded before other Bag files +-------------------------------------------------------------------------------- + +SFrames.Bags = SFrames.Bags or {} +SFrames.Bags.Offline = {} + +local offlineFrame = CreateFrame("Frame") +offlineFrame:RegisterEvent("PLAYER_LOGIN") +offlineFrame:RegisterEvent("BAG_UPDATE") +offlineFrame:RegisterEvent("BANKFRAME_OPENED") +offlineFrame:RegisterEvent("BANKFRAME_CLOSED") +offlineFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED") +offlineFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED") + +local realmName = "" +local playerName = "" +local isBankOpen = false + +local function QueueBankRefreshScans() + local elapsed = 0 + local index = 1 + local points = { 0.08, 0.25, 0.55, 0.95 } + local t = CreateFrame("Frame") + t:SetScript("OnUpdate", function() + elapsed = elapsed + (arg1 or 0) + if index <= table.getn(points) and elapsed >= points[index] then + if isBankOpen then + SFrames.Bags.Offline:ScanBags() + end + index = index + 1 + end + if index > table.getn(points) then + this:SetScript("OnUpdate", nil) + end + end) +end + +local function InitDB() + if not SFramesGlobalDB then SFramesGlobalDB = {} end + if not SFramesGlobalDB[realmName] then SFramesGlobalDB[realmName] = {} end + if not SFramesGlobalDB[realmName][playerName] then + SFramesGlobalDB[realmName][playerName] = { + bags = {}, + bank = {}, + bankSlots = 0, + bankBags = {}, + equippedBags = {}, + money = 0 + } + end +end + +local function IsBankBagInvSlot(slotID) + return type(slotID) == "number" and slotID > 23 +end + +local function IsBagItemLink(link) + if type(link) ~= "string" or link == "" then + return false + end + local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link) + return equipLoc == "INVTYPE_BAG" +end + +local function GetBagLinkState(link) + if type(link) ~= "string" or link == "" then + return "invalid" + end + local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link) + if equipLoc == "INVTYPE_BAG" then + return "bag" + end + if equipLoc == nil or equipLoc == "" then + return "unknown" + end + return "invalid" +end + +local function SafeGetInventorySlotByName(slotName) + if not GetInventorySlotInfo then return nil end + local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end) + if ok and IsBankBagInvSlot(slotID) then + return slotID + end + return nil +end + +local function ResolvePlayerBagInvSlot(index) + if type(index) ~= "number" or index < 1 or index > 4 then + return nil + end + + local liveBtn = _G["CharacterBag" .. (index - 1) .. "Slot"] + if liveBtn and liveBtn.GetID then + local id = liveBtn:GetID() + if type(id) == "number" and id > 0 then + return id + end + end + + if ContainerIDToInventoryID then + local ok, slotID = pcall(function() return ContainerIDToInventoryID(index) end) + if ok and type(slotID) == "number" and slotID > 0 then + return slotID + end + end + + local slotNames = { + "Bag" .. (index - 1) .. "Slot", + "Bag" .. index .. "Slot", + "CharacterBag" .. (index - 1) .. "Slot", + } + for _, slotName in ipairs(slotNames) do + local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end) + if ok and type(slotID) == "number" and slotID > 0 then + return slotID + end + end + + -- Vanilla fallback. + local fallback = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 } + return fallback[index] +end + +local function CaptureEquippedBagMeta(db) + if not db then return end + + db.equippedBags = {} + for index = 1, 4 do + local invSlot = ResolvePlayerBagInvSlot(index) + local link = nil + local texture = nil + if invSlot then + link = GetInventoryItemLink("player", invSlot) + texture = GetInventoryItemTexture("player", invSlot) + end + + local size = GetContainerNumSlots(index) or 0 + db.equippedBags[index] = { + link = link, + texture = texture, + size = tonumber(size) or 0, + } + end +end + +local function ResolveBankBagInvSlot(index) + if type(index) ~= "number" or index <= 0 then return nil end + local bagID = index + 4 + local function TryBankButtonID(buttonID, isBank) + if not BankButtonIDToInvSlotID then return nil end + local ok, slotID = pcall(function() return BankButtonIDToInvSlotID(buttonID, isBank) end) + if ok and IsBankBagInvSlot(slotID) then + return slotID + end + return nil + end + + local function AcceptIfBag(slotID) + if not IsBankBagInvSlot(slotID) then return nil end + local link = GetInventoryItemLink("player", slotID) + if link and IsBagItemLink(link) then + return slotID + end + return nil + end + + -- Guda-style primary mapping + local slot = TryBankButtonID(bagID, 1) + if slot then return slot end + slot = TryBankButtonID(index, 1) + if slot then return slot end + + -- Fallbacks: only when they are confirmed bag items + slot = TryBankButtonID(bagID, nil) + slot = AcceptIfBag(slot) + if slot then return slot end + + slot = TryBankButtonID(index, nil) + slot = AcceptIfBag(slot) + if slot then return slot end + + if ContainerIDToInventoryID then + local ok, s = pcall(function() return ContainerIDToInventoryID(bagID) end) + if ok then + slot = AcceptIfBag(s) + if slot then return slot end + end + end + + slot = AcceptIfBag(SafeGetInventorySlotByName("BankBagSlot" .. index)) + if slot then return slot end + slot = AcceptIfBag(SafeGetInventorySlotByName("BankSlot" .. index)) + if slot then return slot end + + local liveBtn = _G["BankFrameBag" .. index] or _G["BankFrameBag" .. index .. "Slot"] + if liveBtn and liveBtn.GetID then + slot = AcceptIfBag(liveBtn:GetID()) + if slot then return slot end + end + + return nil +end + +local function CaptureBankBagMeta(db) + if not db then return end + + local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0 + db.bankSlots = tonumber(purchased) or 0 + db.bankBags = {} + + for index = 1, 7 do + local bagID = index + 4 + local size = GetContainerNumSlots(bagID) or 0 + local info = { + unlocked = (index <= db.bankSlots), + size = tonumber(size) or 0, + link = nil, + texture = nil, + } + + if info.unlocked then + local invSlot = ResolveBankBagInvSlot(index) + if invSlot then + local link = GetInventoryItemLink("player", invSlot) + local state = GetBagLinkState(link) + if (state == "bag") or (state == "unknown" and info.size > 0) then + info.link = link + info.texture = GetInventoryItemTexture("player", invSlot) + end + end + end + + db.bankBags[index] = info + end +end + +local function ScanContainer(containerID, isBank) + if not SFramesGlobalDB or realmName == "" or playerName == "" then return end + local db = SFramesGlobalDB[realmName][playerName] + local targetDB = isBank and db.bank or db.bags + local size = GetContainerNumSlots(containerID) + targetDB[containerID] = { size = size, items = {} } + for slot = 1, size do + local link = GetContainerItemLink(containerID, slot) + if link then + local texture, itemCount, locked, quality = GetContainerItemInfo(containerID, slot) + local _, _, idStr = string.find(link, "item:(%d+):") + local itemID = idStr and tonumber(idStr) or nil + targetDB[containerID].items[slot] = { id = itemID, + link = link, + count = itemCount, + texture = texture, + quality = quality + } + end + end +end + +function SFrames.Bags.Offline:ScanBags() + if realmName == "" or playerName == "" then return end + InitDB() + local db = SFramesGlobalDB[realmName] and SFramesGlobalDB[realmName][playerName] + if not db then return end + + for bag = 0, 4 do + ScanContainer(bag, false) + end + CaptureEquippedBagMeta(db) + if isBankOpen then + ScanContainer(-1, true) + for bag = 5, 11 do + ScanContainer(bag, true) + end + CaptureBankBagMeta(db) + end + db.money = GetMoney() +end + +local scanPending = false +local scanTimer = CreateFrame("Frame") +scanTimer:Hide() +local function RequestBagScan() + if scanPending then return end + scanPending = true + scanTimer.elapsed = 0 + scanTimer:Show() + scanTimer:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + arg1 + if this.elapsed > 0.5 then + this:SetScript("OnUpdate", nil) + this:Hide() + scanPending = false + SFrames.Bags.Offline:ScanBags() + end + end) +end + +offlineFrame:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + realmName = GetRealmName() or "" + playerName = UnitName("player") or "" + InitDB() + SFrames.Bags.Offline:ScanBags() + elseif event == "BAG_UPDATE" or event == "PLAYERBANKSLOTS_CHANGED" or event == "PLAYERBANKBAGSLOTS_CHANGED" then + RequestBagScan() + elseif event == "BANKFRAME_OPENED" then + isBankOpen = true + RequestBagScan() + QueueBankRefreshScans() + elseif event == "BANKFRAME_CLOSED" then + isBankOpen = false + end +end) + +function SFrames.Bags.Offline:GetCharacterList() + if not SFramesGlobalDB or realmName == "" then return {} end + local chars = {} + if SFramesGlobalDB[realmName] then + for name in pairs(SFramesGlobalDB[realmName]) do + table.insert(chars, name) + end + end + return chars +end + +function SFrames.Bags.Offline:GetCharacterData(name) + if not SFramesGlobalDB or realmName == "" then return nil end + return SFramesGlobalDB[realmName] and SFramesGlobalDB[realmName][name] +end + +function SFrames.Bags.Offline:GetCurrentPlayerName() + return playerName +end diff --git a/Bags/Sort.lua b/Bags/Sort.lua new file mode 100644 index 0000000..aba630a --- /dev/null +++ b/Bags/Sort.lua @@ -0,0 +1,483 @@ +-------------------------------------------------------------------------------- +-- S-Frames: Bag Sorting Logic (Bags/Sort.lua) +-- Manual sorting algorithm compatible with Vanilla/Turtle WoW +-------------------------------------------------------------------------------- + +SFrames.Bags.Sort = SFrames.Bags.Sort or {} + +local isSorting = false +local sortQueue = {} +local sortTimer = CreateFrame("Frame") +local sortDelay = 0.05 -- Increased speed from 0.15 to accelerate sorting. +local timeSinceLast = 0 +local activeCompleteMessage = nil +local activeCompleteUpdate = nil +local activeBagOrder = nil +local activePhase = nil +local TEXT_LOCKED = "\233\131\168\229\136\134\231\137\169\229\147\129\229\183\178\233\148\129\229\174\154\239\188\140\232\175\183\231\168\141\229\144\142\233\135\141\232\175\149\227\128\130" +local TEXT_OFFLINE_BAGS = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\230\151\160\230\179\149\230\149\180\231\144\134\229\156\168\231\186\191\232\131\140\229\140\133\227\128\130" +local TEXT_BAG_DONE = "\232\131\140\229\140\133\230\149\180\231\144\134\229\174\140\230\136\144\227\128\130" +local TEXT_OFFLINE_BANK = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\230\151\160\230\179\149\230\149\180\231\144\134\229\156\168\231\186\191\233\147\182\232\161\140\227\128\130" +local TEXT_BANK_DONE = "\233\147\182\232\161\140\230\149\180\231\144\134\229\174\140\230\136\144\227\128\130" +local itemMaxStackCache = {} + +local function GetItemSortValue(link) + if not link then return 0, "" end + + local _, _, itemString = string.find(link, "^|c%x+|H(.+)|h%[.*%]") + if not itemString then itemString = link end + + local itemName, _, itemRarity, _, itemType = GetItemInfo(itemString) + if not itemName then return 0, "" end + + local score = 0 + local _, _, itemID = string.find(link, "item:(%d+)") + if itemID == "6948" or string.find(itemName, "Hearthstone") then + score = 1000000 + else + score = score + (itemRarity or 0) * 100000 + + if itemType == "Weapon" then + score = score + 50000 + elseif itemType == "Armor" then + score = score + 40000 + elseif itemType == "Consumable" then + score = score + 30000 + elseif itemType == "Trade Goods" then + score = score + 20000 + elseif itemType == "Recipe" then + score = score + 10000 + end + end + + return score, itemName +end + +local function GetItemIdentity(link) + if not link then return nil end + local _, _, itemString = string.find(link, "|H([^|]+)|h") + if not itemString then itemString = link end + return itemString +end + +local function GetItemMaxStack(link) + local itemKey = GetItemIdentity(link) + if not itemKey then return 1 end + + if itemMaxStackCache[itemKey] then + return itemMaxStackCache[itemKey] + end + + local maxStack = 1 + local res = {GetItemInfo(itemKey)} + -- In standard vanilla, maxStack is parameter 7. In some modified schema (TBC) it is parameter 8. + local v7 = tonumber(res[7]) + local v8 = tonumber(res[8]) + if v7 and v7 > 1 then + maxStack = v7 + elseif v8 and v8 > 1 then + maxStack = v8 + end + + itemMaxStackCache[itemKey] = maxStack + return maxStack +end + +local function GetSpecialType(link, isBag) + if not link then return "Normal" end + local _, _, itemString = string.find(link, "^|c%x+|H(.+)|h%[.*%]") + if not itemString then itemString = link end + local itemName, _, _, _, itemType, itemSubType = GetItemInfo(itemString) + + if not itemType and not itemName then return "Normal" end + + local _, _, itemID = string.find(itemString, "item:(%d+)") + + if isBag then + if itemType == "Quiver" or itemType == "箭袋" then return "Ammo" end + if itemSubType == "Quiver" or itemSubType == "Ammo Pouch" or itemSubType == "箭袋" or itemSubType == "子弹袋" then return "Ammo" end + if itemSubType == "Soul Bag" or itemSubType == "灵魂袋" then return "Soul" end + if itemSubType == "Enchanting Bag" or itemSubType == "附魔材料袋" then return "Enchanting" end + if itemSubType == "Herb Bag" or itemSubType == "草药袋" then return "Herb" end + else + if itemType == "Projectile" or itemType == "弹药" then return "Ammo" end + if itemSubType == "Arrow" or itemSubType == "Bullet" or itemSubType == "箭" or itemSubType == "子弹" then return "Ammo" end + if itemID == "6265" or itemName == "Soul Shard" or itemName == "灵魂碎片" then return "Soul" end + end + + return "Normal" +end + +local function SortItemsByRule(items) + table.sort(items, function(a, b) + if a.score ~= b.score then + return a.score > b.score + elseif a.name ~= b.name then + return a.name < b.name + else + return a.count > b.count + end + end) +end + +local function ResetSortState() + isSorting = false + sortQueue = {} + activeCompleteMessage = nil + activeCompleteUpdate = nil + activeBagOrder = nil + activePhase = nil + timeSinceLast = 0 + sortTimer:Hide() +end + +local function FinishSort() + local msg = activeCompleteMessage + local updater = activeCompleteUpdate + ResetSortState() + + if msg then SFrames:Print(msg) end + if updater then updater() end +end + +function SFrames.Bags.Sort:ExecuteSimpleSort(items, bagOrder) + sortQueue = {} + + local slotPool = { Normal = {}, Ammo = {}, Soul = {}, Enchanting = {}, Herb = {} } + local allocatedStream = {} + + for _, bag in ipairs(bagOrder) do + local btype = "Normal" + if bag ~= 0 and bag ~= -1 then + local invID + if bag >= 1 and bag <= 4 then + invID = ContainerIDToInventoryID(bag) + elseif bag >= 5 and bag <= 11 then + invID = bag + 34 + end + if invID then + local ok, link = pcall(GetInventoryItemLink, "player", invID) + if ok and link then btype = GetSpecialType(link, true) end + end + end + + local bagSlots = GetContainerNumSlots(bag) or 0 + for slot = 1, bagSlots do + local s = { bag = bag, slot = slot, type = btype } + table.insert(allocatedStream, s) + if slotPool[btype] then + table.insert(slotPool[btype], s) + else + table.insert(slotPool.Normal, s) + end + end + end + + for _, item in ipairs(items) do + item.specialType = GetSpecialType(item.link, false) + end + + -- Now assign target slot to each item + for _, item in ipairs(items) do + local targetSlot = nil + + if item.specialType ~= "Normal" and slotPool[item.specialType] and table.getn(slotPool[item.specialType]) > 0 then + targetSlot = table.remove(slotPool[item.specialType], 1) + elseif table.getn(slotPool.Normal) > 0 then + targetSlot = table.remove(slotPool.Normal, 1) + end + + if targetSlot then + targetSlot.idealItem = item + end + end + + -- Build virtual moves to align physical items to targetSlots + for _, target in ipairs(allocatedStream) do + if target.idealItem then + local idealItem = target.idealItem + if idealItem.bag ~= target.bag or idealItem.slot ~= target.slot then + table.insert(sortQueue, { + fromBag = idealItem.bag, + fromSlot = idealItem.slot, + toBag = target.bag, + toSlot = target.slot, + retries = 0 + }) + + -- Reflect the virtual swap in our model so later moves stay correct. + for j = 1, table.getn(items) do + if items[j].bag == target.bag and items[j].slot == target.slot then + items[j].bag = idealItem.bag + items[j].slot = idealItem.slot + break + end + end + + idealItem.bag = target.bag + idealItem.slot = target.slot + end + end + end +end + +function SFrames.Bags.Sort:ScanItems(bagOrder, ignoreLocked) + local items = {} + + for _, bag in ipairs(bagOrder) do + local bagSlots = GetContainerNumSlots(bag) or 0 + for slot = 1, bagSlots do + local link = GetContainerItemLink(bag, slot) + local _, itemCount, locked = GetContainerItemInfo(bag, slot) + + if locked and not ignoreLocked then + return nil, TEXT_LOCKED + end + + if link then + local score, name = GetItemSortValue(link) + table.insert(items, { + bag = bag, + slot = slot, + link = link, + score = score, + name = name, + count = itemCount or 1, + stackKey = GetItemIdentity(link), + maxStack = GetItemMaxStack(link) + }) + end + end + end + + return items, nil +end + +function SFrames.Bags.Sort:BuildStackMergeMoves(items) + local grouped = {} + local moves = {} + + for _, item in ipairs(items) do + if item and item.stackKey and item.maxStack and item.maxStack > 1 and (item.count or 0) > 0 then + if not grouped[item.stackKey] then + grouped[item.stackKey] = { + maxStack = item.maxStack, + slots = {} + } + end + + if item.maxStack > grouped[item.stackKey].maxStack then + grouped[item.stackKey].maxStack = item.maxStack + end + + table.insert(grouped[item.stackKey].slots, { + bag = item.bag, + slot = item.slot, + count = item.count + }) + end + end + + for _, group in pairs(grouped) do + local slots = group.slots + if table.getn(slots) > 1 then + table.sort(slots, function(a, b) + if a.count ~= b.count then + return a.count > b.count + elseif a.bag ~= b.bag then + return a.bag < b.bag + else + return a.slot < b.slot + end + end) + + local left = 1 + local right = table.getn(slots) + while left < right do + local target = slots[left] + local source = slots[right] + + if target.count >= group.maxStack then + left = left + 1 + elseif source.count <= 0 then + right = right - 1 + else + local transfer = math.min(group.maxStack - target.count, source.count) + if transfer > 0 then + table.insert(moves, { + fromBag = source.bag, + fromSlot = source.slot, + toBag = target.bag, + toSlot = target.slot, + transferCount = transfer, + isStackMove = true + }) + target.count = target.count + transfer + source.count = source.count - transfer + end + + if source.count <= 0 then + right = right - 1 + end + if target.count >= group.maxStack then + left = left + 1 + end + end + end + end + end + + return moves +end + +function SFrames.Bags.Sort:StartPlacementPhase(ignoreLocked) + if not activeBagOrder then + FinishSort() + return + end + + local items, err = self:ScanItems(activeBagOrder, ignoreLocked) + if not items then + ResetSortState() + if err then SFrames:Print(err) end + return + end + + SortItemsByRule(items) + self:ExecuteSimpleSort(items, activeBagOrder) + + if table.getn(sortQueue) == 0 then + FinishSort() + return + end + + activePhase = "sort" + sortTimer:Show() +end + +function SFrames.Bags.Sort:StartForBags(bagOrder, completeMessage, completeUpdate) + if isSorting then return end + + isSorting = true + sortQueue = {} + timeSinceLast = 0 + activeCompleteMessage = completeMessage + activeCompleteUpdate = completeUpdate + activeBagOrder = bagOrder + activePhase = nil + + local items, err = self:ScanItems(bagOrder, false) + if not items then + ResetSortState() + if err then SFrames:Print(err) end + return + end + + local stackMoves = self:BuildStackMergeMoves(items) + if table.getn(stackMoves) > 0 then + sortQueue = stackMoves + activePhase = "stack" + sortTimer:Show() + return + end + + self:StartPlacementPhase(false) +end + +function SFrames.Bags.Sort:Start() + if SFrames.Bags.Container and SFrames.Bags.Container.isOffline then + SFrames:Print(TEXT_OFFLINE_BAGS) + return + end + + self:StartForBags( + {0, 1, 2, 3, 4}, + TEXT_BAG_DONE, + function() + if SFrames.Bags.Container then + SFrames.Bags.Container:UpdateLayout() + end + end + ) +end + +function SFrames.Bags.Sort:StartBank() + if SFrames.Bags.Bank and SFrames.Bags.Bank.isOffline then + SFrames:Print(TEXT_OFFLINE_BANK) + return + end + + self:StartForBags( + {-1, 5, 6, 7, 8, 9, 10, 11}, + TEXT_BANK_DONE, + function() + if SFrames.Bags.Bank then + SFrames.Bags.Bank:UpdateLayout() + end + end + ) +end + +sortTimer:SetScript("OnUpdate", function() + if not isSorting then + this:Hide() + return + end + + timeSinceLast = timeSinceLast + arg1 + if timeSinceLast > sortDelay then + timeSinceLast = 0 + + if table.getn(sortQueue) > 0 then + local move = sortQueue[1] + local _, _, lockedFrom = GetContainerItemInfo(move.fromBag, move.fromSlot) + local _, _, lockedTo = GetContainerItemInfo(move.toBag, move.toSlot) + + if lockedFrom or lockedTo or CursorHasItem() then + move.retries = (move.retries or 0) + 1 + if move.retries > 40 then + table.remove(sortQueue, 1) + if CursorHasItem() then + PickupContainerItem(move.fromBag, move.fromSlot) + end + end + return + end + + table.remove(sortQueue, 1) + if move.isStackMove and move.transferCount then + SplitContainerItem(move.fromBag, move.fromSlot, move.transferCount) + PickupContainerItem(move.toBag, move.toSlot) + else + PickupContainerItem(move.fromBag, move.fromSlot) + PickupContainerItem(move.toBag, move.toSlot) + if CursorHasItem() then + PickupContainerItem(move.fromBag, move.fromSlot) + if CursorHasItem() then + PickupContainerItem(move.toBag, move.toSlot) + end + end + end + else + if activePhase == "stack" then + if activeBagOrder then + for _, bag in ipairs(activeBagOrder) do + local bagSlots = GetContainerNumSlots(bag) or 0 + for slot = 1, bagSlots do + local _, _, locked = GetContainerItemInfo(bag, slot) + if locked then + return + end + end + end + end + -- After consolidation, rescan inventory and run normal sorting. + SFrames.Bags.Sort:StartPlacementPhase(true) + else + if CursorHasItem() then return end + FinishSort() + end + end + end +end) +sortTimer:Hide() diff --git a/Bindings.xml b/Bindings.xml new file mode 100644 index 0000000..2e26d22 --- /dev/null +++ b/Bindings.xml @@ -0,0 +1,7 @@ + + + if SFrames and SFrames.WorldMap and SFrames.WorldMap.ToggleNav then + SFrames.WorldMap:ToggleNav() + end + + diff --git a/BookUI.lua b/BookUI.lua new file mode 100644 index 0000000..badbb08 --- /dev/null +++ b/BookUI.lua @@ -0,0 +1,375 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Book Reading UI (BookUI.lua) +-- Replaces ItemTextFrame with Nanami-UI styled interface + page flipping +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.BookUI = {} +local BUI = SFrames.BookUI + +-------------------------------------------------------------------------------- +-- Theme (match QuestUI style) +-------------------------------------------------------------------------------- +local T = SFrames.ActiveTheme + +-------------------------------------------------------------------------------- +-- Layout +-------------------------------------------------------------------------------- +local FRAME_W = 340 +local FRAME_H = 400 +local HEADER_H = 34 +local BOTTOM_H = 42 +local SIDE_PAD = 14 +local CONTENT_W = FRAME_W - SIDE_PAD * 2 +local SCROLL_STEP = 40 +local BODY_FONT_SIZE = 12 +local BODY_LINE_SPACING = 2 + +-------------------------------------------------------------------------------- +-- State +-------------------------------------------------------------------------------- +local MainFrame = nil + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) +end + +local function CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.45) + s:SetBackdropBorderColor(0, 0, 0, 0.6) + s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) + return s +end + +local function TextHeight(fs, fallback) + if not fs then return fallback or 14 end + local h = fs.GetStringHeight and fs:GetStringHeight() + if h and h > 0 then return h end + h = fs:GetHeight() + if h and h > 1 then return h end + return fallback or 14 +end + +local function IsTrue(v) + return v == true or v == 1 +end + +local function CreateScrollArea(parent, name) + local scroll = CreateFrame("ScrollFrame", name, parent) + local content = CreateFrame("Frame", name .. "Content", scroll) + content:SetWidth(CONTENT_W) + content:SetHeight(1) + scroll:SetScrollChild(content) + + scroll:EnableMouseWheel(true) + scroll:SetScript("OnMouseWheel", function() + local cur = this:GetVerticalScroll() + local maxVal = this:GetVerticalScrollRange() + if arg1 > 0 then + this:SetVerticalScroll(math.max(0, cur - SCROLL_STEP)) + else + this:SetVerticalScroll(math.min(maxVal, cur + SCROLL_STEP)) + end + end) + + scroll.content = content + return scroll +end + +local function CreateActionBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 90) + btn:SetHeight(28) + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 11, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + fs:SetText(text or "") + btn.label = fs + + btn.disabled = false + function btn:SetDisabled(flag) + self.disabled = flag + if flag then + self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) + else + self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + end + end + + btn:SetScript("OnEnter", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end + end) + btn:SetScript("OnLeave", function() + if not this.disabled then + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end) + btn:SetScript("OnMouseDown", function() + if not this.disabled then + this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) + end + end) + btn:SetScript("OnMouseUp", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + end + end) + + return btn +end + +local function HideBlizzardItemText() + if not ItemTextFrame then return end + ItemTextFrame:SetAlpha(0) + ItemTextFrame:EnableMouse(false) + ItemTextFrame:ClearAllPoints() + ItemTextFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) +end + +local function UpdateBookContent() + if not MainFrame then return end + + local title = ItemTextGetItem and (ItemTextGetItem() or "") or "" + local text = ItemTextGetText and (ItemTextGetText() or "") or "" + if (not text or text == "") and ItemTextPageText and ItemTextPageText.GetText then + text = ItemTextPageText:GetText() or "" + end + if text and text ~= "" then + -- Keep paragraph gap readable: collapse 3+ consecutive newlines. + text = string.gsub(text, "\n%s*\n%s*\n+", "\n\n") + end + local pageNum = ItemTextGetPage and tonumber(ItemTextGetPage()) or 1 + if not pageNum or pageNum < 1 then pageNum = 1 end + + MainFrame.titleFS:SetText(title) + MainFrame.pageFS:SetText("第 " .. pageNum .. " 页") + MainFrame.bodyFS:SetText(text) + + MainFrame.bodyFS:ClearAllPoints() + MainFrame.bodyFS:SetPoint("TOPLEFT", MainFrame.scroll.content, "TOPLEFT", 2, -6) + MainFrame.scroll.content:SetHeight(TextHeight(MainFrame.bodyFS, 14) + 18) + MainFrame.scroll:SetVerticalScroll(0) + + local hasPrevApi = ItemTextHasPrevPage and IsTrue(ItemTextHasPrevPage()) or false + local hasNextApi = ItemTextHasNextPage and IsTrue(ItemTextHasNextPage()) or false + local canPrev = (pageNum and pageNum > 1) or hasPrevApi + MainFrame.prevBtn:SetDisabled(not canPrev) + MainFrame.nextBtn:SetDisabled(not hasNextApi) +end + +local function QueueRefresh(delay, count) + if not MainFrame then return end + MainFrame._refreshDelay = delay or 0.05 + MainFrame._refreshCount = count or 1 +end + +local function CloseBookFrame() + if MainFrame and MainFrame:IsVisible() then + MainFrame:Hide() + end + if CloseItemText then + pcall(CloseItemText) + elseif ItemTextFrame and ItemTextFrame.Hide then + pcall(ItemTextFrame.Hide, ItemTextFrame) + end +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function BUI:Initialize() + if MainFrame then return end + + MainFrame = CreateFrame("Frame", "SFramesBookFrame", UIParent) + MainFrame:SetWidth(FRAME_W) + MainFrame:SetHeight(FRAME_H) + MainFrame:SetPoint("LEFT", UIParent, "LEFT", 64, 0) + MainFrame:SetFrameStrata("HIGH") + MainFrame:SetToplevel(true) + MainFrame:EnableMouse(true) + MainFrame:SetMovable(true) + MainFrame:RegisterForDrag("LeftButton") + MainFrame:SetScript("OnDragStart", function() this:StartMoving() end) + MainFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + SetRoundBackdrop(MainFrame) + CreateShadow(MainFrame) + + local header = CreateFrame("Frame", nil, MainFrame) + header:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 0, 0) + header:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 0, 0) + header:SetHeight(HEADER_H) + + local titleFS = header:CreateFontString(nil, "OVERLAY") + titleFS:SetFont(GetFont(), 14, "OUTLINE") + titleFS:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0) + titleFS:SetPoint("RIGHT", header, "RIGHT", -96, 0) + titleFS:SetJustifyH("LEFT") + titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + MainFrame.titleFS = titleFS + + local pageFS = header:CreateFontString(nil, "OVERLAY") + pageFS:SetFont(GetFont(), 11, "OUTLINE") + pageFS:SetPoint("RIGHT", header, "RIGHT", -30, 0) + pageFS:SetJustifyH("RIGHT") + pageFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + MainFrame.pageFS = pageFS + + local closeBtn = CreateFrame("Button", nil, MainFrame, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 2, 2) + closeBtn:SetWidth(24); closeBtn:SetHeight(24) + + local headerSep = MainFrame:CreateTexture(nil, "ARTWORK") + headerSep:SetTexture("Interface\\Buttons\\WHITE8X8") + headerSep:SetHeight(1) + headerSep:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 6, -HEADER_H) + headerSep:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -6, -HEADER_H) + headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + local contentArea = CreateFrame("Frame", nil, MainFrame) + contentArea:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -(HEADER_H + 4)) + contentArea:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, BOTTOM_H + 4) + MainFrame.contentArea = contentArea + + local scroll = CreateScrollArea(contentArea, "SFramesBookScroll") + scroll:SetPoint("TOPLEFT", contentArea, "TOPLEFT", 0, 0) + scroll:SetPoint("BOTTOMRIGHT", contentArea, "BOTTOMRIGHT", 0, 0) + MainFrame.scroll = scroll + + local bodyFS = scroll.content:CreateFontString(nil, "OVERLAY") + bodyFS:SetFont(GetFont(), BODY_FONT_SIZE) + if bodyFS.SetSpacing then + bodyFS:SetSpacing(BODY_LINE_SPACING) + end + bodyFS:SetWidth(CONTENT_W - 4) + bodyFS:SetJustifyH("LEFT") + bodyFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + MainFrame.bodyFS = bodyFS + + local bottomSep = MainFrame:CreateTexture(nil, "ARTWORK") + bottomSep:SetTexture("Interface\\Buttons\\WHITE8X8") + bottomSep:SetHeight(1) + bottomSep:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", 6, BOTTOM_H) + bottomSep:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -6, BOTTOM_H) + bottomSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + MainFrame.prevBtn = CreateActionBtn(MainFrame, "上一页", 90) + MainFrame.prevBtn:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, 8) + MainFrame.prevBtn:SetScript("OnClick", function() + if this.disabled then return end + if ItemTextPrevPage then + ItemTextPrevPage() + QueueRefresh(0.05, 5) + end + end) + + MainFrame.closeBtn = CreateActionBtn(MainFrame, "关闭", 90) + MainFrame.closeBtn:SetPoint("BOTTOM", MainFrame, "BOTTOM", 0, 8) + MainFrame.closeBtn:SetScript("OnClick", function() + CloseBookFrame() + end) + + MainFrame.nextBtn = CreateActionBtn(MainFrame, "下一页", 90) + MainFrame.nextBtn:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, 8) + MainFrame.nextBtn:SetScript("OnClick", function() + if this.disabled then return end + if ItemTextNextPage then + ItemTextNextPage() + QueueRefresh(0.05, 5) + end + end) + + MainFrame:SetScript("OnHide", function() + if ItemTextFrame and ItemTextFrame:IsVisible() then + pcall(ItemTextFrame.Hide, ItemTextFrame) + end + end) + MainFrame:SetScript("OnUpdate", function() + if not this._refreshCount or this._refreshCount <= 0 then return end + this._refreshDelay = (this._refreshDelay or 0) - (arg1 or 0) + if this._refreshDelay > 0 then return end + UpdateBookContent() + this._refreshCount = this._refreshCount - 1 + if this._refreshCount > 0 then + this._refreshDelay = 0.08 + end + end) + + MainFrame:RegisterEvent("ITEM_TEXT_BEGIN") + MainFrame:RegisterEvent("ITEM_TEXT_READY") + MainFrame:RegisterEvent("ITEM_TEXT_CLOSED") + MainFrame:SetScript("OnEvent", function() + if event == "ITEM_TEXT_BEGIN" then + HideBlizzardItemText() + UpdateBookContent() + QueueRefresh(0.05, 5) + MainFrame:Show() + elseif event == "ITEM_TEXT_READY" then + HideBlizzardItemText() + UpdateBookContent() + QueueRefresh(0.05, 5) + if not MainFrame:IsVisible() then + MainFrame:Show() + end + elseif event == "ITEM_TEXT_CLOSED" then + MainFrame._refreshCount = 0 + MainFrame:Hide() + end + end) + + MainFrame:Hide() + tinsert(UISpecialFrames, "SFramesBookFrame") +end + +-------------------------------------------------------------------------------- +-- Bootstrap +-------------------------------------------------------------------------------- +local bootstrap = CreateFrame("Frame") +bootstrap:RegisterEvent("PLAYER_LOGIN") +bootstrap:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + BUI:Initialize() + end +end) diff --git a/CharacterPanel.lua b/CharacterPanel.lua new file mode 100644 index 0000000..c1440c7 --- /dev/null +++ b/CharacterPanel.lua @@ -0,0 +1,3320 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Character Panel (CharacterPanel.lua) +-- Replaces the default CharacterFrame with a modern rounded UI +-------------------------------------------------------------------------------- + +SFrames.CharacterPanel = {} + +local CP = SFrames.CharacterPanel +local panel, tabs, pages + +-------------------------------------------------------------------------------- +-- Theme +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + repColors = { + [1] = { 0.8, 0.13, 0.13 }, [2] = { 0.8, 0.3, 0.22 }, + [3] = { 0.75, 0.55, 0.15 }, [4] = { 0.9, 0.7, 0.0 }, + [5] = { 0.0, 0.6, 0.1 }, [6] = { 0.0, 0.7, 0.3 }, + [7] = { 0.0, 0.6, 0.75 }, [8] = { 0.0, 0.5, 0.9 }, + }, + resistColors = { + [2] = { 1, 0.5, 0.15 }, [3] = { 0.35, 0.9, 0.25 }, + [4] = { 0.45, 0.7, 1 }, [5] = { 0.6, 0.35, 0.9 }, + [6] = { 0.95, 0.9, 0.4 }, + }, +}) + +local FRAME_W = 340 +local FRAME_H = 490 +local HEADER_H = 28 +local TAB_H = 22 +local TAB_BAR_H = 26 +local CONTENT_TOP = -(HEADER_H + TAB_BAR_H) +local SIDE_PAD = 8 +local INNER_PAD = 4 +local CONTENT_W = FRAME_W - INNER_PAD * 2 +local SCROLL_W = CONTENT_W - 10 + +local SLOT_SIZE = 32 +local SLOT_GAP = 2 + +local QUALITY_COLORS = { + [0] = { 0.62, 0.62, 0.62 }, [1] = { 1, 1, 1 }, + [2] = { 0.12, 1, 0 }, [3] = { 0.0, 0.44, 0.87 }, + [4] = { 0.64, 0.21, 0.93 }, [5] = { 1, 0.5, 0 }, +} + +local SLOT_LABEL = { + HeadSlot = "头部", NeckSlot = "颈部", ShoulderSlot = "肩部", + BackSlot = "背部", ChestSlot = "胸部", ShirtSlot = "衬衣", + WristSlot = "手腕", HandsSlot = "手套", WaistSlot = "腰带", + LegsSlot = "腿部", FeetSlot = "脚部", + Finger0Slot = "戒指1", Finger1Slot = "戒指2", + Trinket0Slot = "饰品1", Trinket1Slot = "饰品2", + MainHandSlot = "主手", SecondaryHandSlot = "副手", + RangedSlot = "远程", AmmoSlot = "弹药", TabardSlot = "战袍", +} + +local EQUIP_SLOTS_LEFT = { + { id = 1, name = "HeadSlot" }, { id = 2, name = "NeckSlot" }, + { id = 3, name = "ShoulderSlot" }, { id = 15, name = "BackSlot" }, + { id = 5, name = "ChestSlot" }, { id = 4, name = "ShirtSlot" }, + { id = 19, name = "TabardSlot" }, { id = 9, name = "WristSlot" }, +} +local EQUIP_SLOTS_RIGHT = { + { id = 10, name = "HandsSlot" }, { id = 6, name = "WaistSlot" }, + { id = 7, name = "LegsSlot" }, { id = 8, name = "FeetSlot" }, + { id = 11, name = "Finger0Slot" }, { id = 12, name = "Finger1Slot" }, + { id = 13, name = "Trinket0Slot" }, { id = 14, name = "Trinket1Slot" }, +} +local EQUIP_SLOTS_BOTTOM = { + { id = 16, name = "MainHandSlot" }, { id = 17, name = "SecondaryHandSlot" }, + { id = 18, name = "RangedSlot" }, { id = 0, name = "AmmoSlot" }, +} + +local TAB_NAMES = { "装备", "声望", "技能", "荣誉" } +local STAT_NAMES = { "力量", "敏捷", "耐力", "智力", "精神" } +local RESIST_NAMES = { [2] = "火焰", [3] = "自然", [4] = "冰霜", [5] = "暗影", [6] = "奥术" } +local REP_STANDING = { + [1] = "仇恨", [2] = "敌对", [3] = "不友好", [4] = "中立", + [5] = "友善", [6] = "尊敬", [7] = "崇敬", [8] = "崇拜", +} + +-------------------------------------------------------------------------------- +-- EP Stat Weights per class (Turtle WoW) +-- Physical DPS: 1 AP = 1 EP; Caster DPS: 1 SP = 1 EP +-- Source: Turtle WoW deep theorycraft & EP evaluation report +-------------------------------------------------------------------------------- +local STAT_WEIGHTS = { + WARRIOR = { + STR = 2.0, AGI = 1.5, STA = 1.0, INT = 0, SPI = 0, + ATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18, + DEFENSE = 1.5, DODGE = 12, PARRY = 12, BLOCK = 8, BLOCKVALUE = 0.5, + ARMOR = 0.02, HEALTHREG = 2, HEALTH = 0.1, + }, + ROGUE = { + STR = 1.0, AGI = 2.0, STA = 0.5, INT = 0, SPI = 0, + ATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18, + DODGE = 10, HEALTH = 0.067, + }, + HUNTER = { + STR = 0.5, AGI = 2.0, STA = 0.5, INT = 0.3, SPI = 0.1, + ATTACKPOWER = 1.0, RANGEDATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18, + RANGEDCRIT = 25, HEALTH = 0.067, MANA = 0.033, + }, + DRUID = { + STR = 2.4, AGI = 1.5, STA = 1.0, INT = 0.5, SPI = 0.5, + ATTACKPOWER = 1.0, ATTACKPOWERFERAL = 1.0, + CRIT = 25, TOHIT = 18, + DMG = 1.0, HEAL = 0.8, SPELLCRIT = 8, SPELLTOHIT = 14, + DEFENSE = 1.2, DODGE = 10, ARMOR = 0.02, + MANAREG = 3, HEALTHREG = 2, HEALTH = 0.1, MANA = 0.033, + }, + PALADIN = { + STR = 2.0, AGI = 1.0, STA = 1.0, INT = 0.5, SPI = 0.3, + ATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18, + DMG = 1.0, HEAL = 0.8, HOLYDMG = 1.0, + SPELLCRIT = 8, SPELLTOHIT = 14, + DEFENSE = 1.5, DODGE = 12, PARRY = 12, BLOCK = 8, BLOCKVALUE = 0.5, + ARMOR = 0.02, MANAREG = 3, HEALTH = 0.1, MANA = 0.033, + }, + SHAMAN = { + STR = 1.0, AGI = 1.0, STA = 0.8, INT = 0.5, SPI = 0.3, + ATTACKPOWER = 1.0, CRIT = 20, TOHIT = 15, + DMG = 1.0, HEAL = 0.8, NATUREDMG = 1.0, + SPELLCRIT = 8, SPELLTOHIT = 14, + MANAREG = 3, HEALTH = 0.1, MANA = 0.033, + }, + MAGE = { + STR = 0, AGI = 0, STA = 0.3, INT = 0.125, SPI = 0.5, + DMG = 1.0, ARCANEDMG = 1.0, FIREDMG = 1.0, FROSTDMG = 1.0, + SPELLCRIT = 8, SPELLTOHIT = 14, + MANAREG = 3, HEALTH = 0.067, MANA = 0.033, + }, + WARLOCK = { + STR = 0, AGI = 0, STA = 0.5, INT = 0.125, SPI = 0.3, + DMG = 1.0, SHADOWDMG = 1.0, FIREDMG = 1.0, + SPELLCRIT = 8, SPELLTOHIT = 14, + MANAREG = 3, HEALTH = 0.1, MANA = 0.033, + }, + PRIEST = { + STR = 0, AGI = 0, STA = 0.3, INT = 0.5, SPI = 1.0, + DMG = 0.8, HEAL = 1.0, SHADOWDMG = 0.8, HOLYDMG = 0.8, + SPELLCRIT = 8, SPELLTOHIT = 14, + MANAREG = 3, HEALTH = 0.067, MANA = 0.033, + }, +} + +local widgetId = 0 + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + local bg = bgColor or T.bg + local bd = borderColor or T.border + frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) + frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) +end + +local function SetPixelBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 } + }) + if bgColor then frame:SetBackdropColor(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1) end + if borderColor then frame:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1) end +end + +local function CreateShadow(parent, size) + local s = CreateFrame("Frame", nil, parent) + local sz = size or 4 + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz) + s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0)) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 } + }) + s:SetBackdropColor(0, 0, 0, 0.6) + s:SetBackdropBorderColor(0, 0, 0, 0.45) + return s +end + +local function MakeSep(parent, x1, y1, x2, y2) + local sep = parent:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + sep:SetHeight(1) + sep:SetPoint("TOPLEFT", parent, "TOPLEFT", x1, y1) + sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", x2, y1) + return sep +end + +local function MakeFS(parent, size, justifyH, color) + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), size or 11, "OUTLINE") + fs:SetJustifyH(justifyH or "LEFT") + local c = color or T.valueText + fs:SetTextColor(c[1], c[2], c[3]) + return fs +end + +local function NextName(p) + widgetId = widgetId + 1 + return "SFramesCP" .. (p or "") .. tostring(widgetId) +end + +local function Clamp(v, lo, hi) + if v < lo then return lo end + if v > hi then return hi end + return v +end + +-------------------------------------------------------------------------------- +-- Combat stat helpers: multi-strategy retrieval for Turtle WoW compatibility +-- 1) Standard 1.12 API (GetCritChance, etc.) +-- 2) GetCombatRatingBonus (TBC-style, Turtle WoW may support) +-- 3) Manual calc: agility/intellect base + ItemBonusLib gear bonus +-------------------------------------------------------------------------------- +local function TryAPIs(names) + for _, name in ipairs(names) do + local fn = _G[name] + if fn then + local ok, val = pcall(fn) + if ok and type(val) == "number" and val > 0 then return val end + end + end + return 0 +end +local function TryAPIsArg(names, arg1) + for _, name in ipairs(names) do + local fn = _G[name] + if fn then + local ok, val = pcall(fn, arg1) + if ok and type(val) == "number" and val > 0 then return val end + end + end + return 0 +end +local function TryCombatRating(crId) + if GetCombatRatingBonus then + local ok, val = pcall(GetCombatRatingBonus, crId) + if ok and type(val) == "number" and val > 0 then return val end + end + return 0 +end + +-- Try to get ItemBonusLib from AceLibrary (provides gear-only crit/hit/etc.) +local function GetItemBonusLib() + if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("ItemBonusLib-1.0") then + return AceLibrary("ItemBonusLib-1.0") + end + return nil +end +local function GetGearBonus(key) + local lib = GetItemBonusLib() + if lib and lib.GetBonus then return lib:GetBonus(key) or 0 end + return 0 +end + +local function CalcItemEP(link, class) + local weights = STAT_WEIGHTS[class] + if not weights or not link then return 0 end + local lib = GetItemBonusLib() + if not lib or not lib.ScanItemLink then return 0 end + local ok, info = pcall(function() return lib:ScanItemLink(link) end) + if not ok or not info or not info.bonuses then return 0 end + local ep = 0 + for stat, value in pairs(info.bonuses) do + local w = weights[stat] + if w and type(value) == "number" then + ep = ep + value * w + end + end + return ep +end + +-- Turtle WoW agility → melee crit (agi per 1% crit at level 60) +local AGI_PER_MELEE_CRIT = { + WARRIOR = 20, PALADIN = 20, ROGUE = 29, HUNTER = 52.91, + DRUID = 20, SHAMAN = 20, MAGE = 0, WARLOCK = 0, PRIEST = 0, +} +local AGI_PER_RANGED_CRIT = { + HUNTER = 52.91, WARRIOR = 20, ROGUE = 29, +} +-- Turtle WoW agility → dodge (agi per 1% dodge at level 60) +local AGI_PER_DODGE = { + ROGUE = 14.5, WARRIOR = 20, PALADIN = 20, + SHAMAN = 20, DRUID = 20, HUNTER = 0, MAGE = 0, WARLOCK = 0, PRIEST = 0, +} +-- Turtle WoW intellect → spell crit (int per 1% crit at level 60) +local INT_PER_SPELL_CRIT = { + MAGE = 59.5, WARLOCK = 60.6, PRIEST = 59.5, + DRUID = 60, SHAMAN = 59.2, PALADIN = 29.5, +} +-- Turtle WoW base melee crit at level 60 (before agility) +local BASE_MELEE_CRIT = { + WARRIOR = 0, PALADIN = 0.7, ROGUE = 0, HUNTER = 0, + DRUID = 0.9, SHAMAN = 1.7, MAGE = 0, WARLOCK = 0, PRIEST = 0, +} +local BASE_SPELL_CRIT = { + MAGE = 0.2, WARLOCK = 1.7, PRIEST = 0.8, + DRUID = 1.8, SHAMAN = 2.3, PALADIN = 0, +} + +local function CalcMeleeCrit() + local _, class = UnitClass("player") + class = class or "" + local baseCrit = BASE_MELEE_CRIT[class] or 0 + local ratio = AGI_PER_MELEE_CRIT[class] or 0 + local agiCrit = 0 + if ratio > 0 then + local _, agi = UnitStat("player", 2) + agiCrit = (agi or 0) / ratio + end + local gearCrit = GetGearBonus("CRIT") + return baseCrit + agiCrit + gearCrit +end +local function CalcRangedCrit() + local _, class = UnitClass("player") + class = class or "" + local baseCrit = BASE_MELEE_CRIT[class] or 0 + local ratio = AGI_PER_RANGED_CRIT[class] or 0 + local agiCrit = 0 + if ratio > 0 then + local _, agi = UnitStat("player", 2) + agiCrit = (agi or 0) / ratio + end + local gearCrit = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT") + return baseCrit + agiCrit + gearCrit +end +local function CalcSpellCrit() + local _, class = UnitClass("player") + class = class or "" + local baseCrit = BASE_SPELL_CRIT[class] or 0 + local ratio = INT_PER_SPELL_CRIT[class] or 0 + local intCrit = 0 + if ratio > 0 then + local _, intel = UnitStat("player", 4) + intCrit = (intel or 0) / ratio + end + local gearCrit = GetGearBonus("SPELLCRIT") + return baseCrit + intCrit + gearCrit +end + +local function SafeGetMeleeCrit() + local v = TryAPIs({ "GetCritChance", "GetMeleeCritChance", "GetPlayerCritChance" }) + if v > 0 then return v end + v = TryCombatRating(_G.CR_CRIT_MELEE or 9) + if v > 0 then return v end + return CalcMeleeCrit() +end +local function SafeGetRangedCrit() + local v = TryAPIs({ "GetRangedCritChance" }) + if v > 0 then return v end + v = TryCombatRating(_G.CR_CRIT_RANGED or 10) + if v > 0 then return v end + return CalcRangedCrit() +end +local function SafeGetSpellCrit() + local v = TryAPIsArg({ "GetSpellCritChance" }, 2) + if v > 0 then return v end + v = TryCombatRating(_G.CR_CRIT_SPELL or 11) + if v > 0 then return v end + return CalcSpellCrit() +end +local function SafeGetMeleeHit() + local v = TryAPIs({ "GetHitModifier", "GetMeleeHitModifier", "GetCombatMissChance" }) + if v > 0 then return v end + v = TryCombatRating(_G.CR_HIT_MELEE or 6) + if v > 0 then return v end + return GetGearBonus("TOHIT") +end +local function SafeGetSpellHit() + local v = TryAPIs({ "GetSpellHitModifier" }) + if v > 0 then return v end + v = TryCombatRating(_G.CR_HIT_SPELL or 8) + if v > 0 then return v end + return GetGearBonus("SPELLTOHIT") +end +local function IsCritFromAPI() + return TryAPIs({ "GetCritChance", "GetMeleeCritChance", "GetPlayerCritChance" }) > 0 + or TryCombatRating(_G.CR_CRIT_MELEE or 9) > 0 +end + +-------------------------------------------------------------------------------- +-- Talent scanning: identify hit/crit bonuses from talent trees +-- Uses both English and Chinese name matching for Turtle WoW compatibility +-------------------------------------------------------------------------------- +local TALENT_DB = { + meleeHit = { + { names = {"Precision", "精准"}, perRank = 1 }, + { names = {"Surefooted", "稳固射击", "脚踏实地"}, perRank = 1 }, + { names = {"Nature's Guidance", "自然指引", "自然引导"}, perRank = 1 }, + }, + spellHit = { + { names = {"Elemental Precision", "元素精准"}, perRank = 2 }, + { names = {"Arcane Focus", "奥术集中"}, perRank = 2 }, + { names = {"Suppression", "镇压"}, perRank = 2 }, + { names = {"Shadow Focus", "暗影集中"}, perRank = 2 }, + { names = {"Nature's Guidance", "自然指引", "自然引导"}, perRank = 1 }, + }, + meleeCrit = { + { names = {"Cruelty", "残忍"}, perRank = 1 }, + { names = {"Malice", "恶意"}, perRank = 1 }, + { names = {"Lethal Shots", "致命射击"}, perRank = 1 }, + { names = {"Conviction", "信念", "坚定信念"}, perRank = 1 }, + { names = {"Sharpened Claws", "利爪强化", "尖锐之爪"}, perRank = 2 }, + { names = {"Thundering Strikes", "雷霆一击", "雷霆之击"}, perRank = 1 }, + }, + spellCrit = { + { names = {"Arcane Instability", "奥术不稳定"}, perRank = 1 }, + { names = {"Critical Mass", "临界质量"}, perRank = 2 }, + { names = {"Holy Specialization", "神圣专精"}, perRank = 1 }, + { names = {"Tidal Mastery", "潮汐掌握"}, perRank = 1 }, + }, +} + +local _talentCache, _talentCacheTime + +local function ScanTalentBonuses() + local now = GetTime and GetTime() or 0 + if _talentCache and _talentCacheTime and (now - _talentCacheTime) < 5 then + return _talentCache + end + local r = { meleeHit = 0, spellHit = 0, meleeCrit = 0, spellCrit = 0, + talentDetails = {} } + if not GetNumTalentTabs then _talentCache = r; _talentCacheTime = now; return r end + + local function NameMatch(tname, patterns) + if not tname then return false end + for _, p in ipairs(patterns) do + if string.find(tname, p, 1, true) then return true end + end + return false + end + + for cat, entries in pairs(TALENT_DB) do + if cat ~= "talentDetails" then + for tab = 1, GetNumTalentTabs() do + for i = 1, GetNumTalents(tab) do + local name, _, _, _, rank, maxRank = GetTalentInfo(tab, i) + if name and rank and rank > 0 then + for _, entry in ipairs(entries) do + if NameMatch(name, entry.names) then + local bonus = rank * entry.perRank + r[cat] = r[cat] + bonus + table.insert(r.talentDetails, + { cat = cat, name = name, + rank = rank, maxRank = maxRank or rank, + bonus = bonus }) + end + end + end + end + end + end + end + _talentCache = r; _talentCacheTime = now + return r +end + +local function GetTalentBonus(cat) + return ScanTalentBonuses()[cat] or 0 +end +local function GetTalentDetailsFor(cat) + local info = ScanTalentBonuses() + local out = {} + for _, d in ipairs(info.talentDetails or {}) do + if d.cat == cat then table.insert(out, d) end + end + return out +end + +-- Comprehensive stat builders (base + agi/int + gear + talent) +local function FullMeleeCrit() + local _, class = UnitClass("player") + class = class or "" + local baseCrit = BASE_MELEE_CRIT[class] or 0 + local ratio = AGI_PER_MELEE_CRIT[class] or 0 + local agiCrit = 0 + if ratio > 0 then + local _, agi = UnitStat("player", 2); agiCrit = (agi or 0) / ratio + end + local gearCrit = GetGearBonus("CRIT") + local talentCrit = GetTalentBonus("meleeCrit") + return baseCrit + agiCrit + gearCrit + talentCrit, + baseCrit, agiCrit, gearCrit, talentCrit +end +local function FullRangedCrit() + local _, class = UnitClass("player") + class = class or "" + local baseCrit = BASE_MELEE_CRIT[class] or 0 + local ratio = AGI_PER_RANGED_CRIT[class] or 0 + local agiCrit = 0 + if ratio > 0 then + local _, agi = UnitStat("player", 2); agiCrit = (agi or 0) / ratio + end + local gearCrit = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT") + local talentCrit = GetTalentBonus("meleeCrit") + return baseCrit + agiCrit + gearCrit + talentCrit, + baseCrit, agiCrit, gearCrit, talentCrit +end +local function FullSpellCrit() + local _, class = UnitClass("player") + class = class or "" + local baseCrit = BASE_SPELL_CRIT[class] or 0 + local ratio = INT_PER_SPELL_CRIT[class] or 0 + local intCrit = 0 + if ratio > 0 then + local _, intel = UnitStat("player", 4); intCrit = (intel or 0) / ratio + end + local gearCrit = GetGearBonus("SPELLCRIT") + local talentCrit = GetTalentBonus("spellCrit") + return baseCrit + intCrit + gearCrit + talentCrit, + baseCrit, intCrit, gearCrit, talentCrit +end +local function FullMeleeHit() + local gearHit = GetGearBonus("TOHIT") + local talentHit = GetTalentBonus("meleeHit") + return gearHit + talentHit, gearHit, talentHit +end +local function FullSpellHit() + local gearHit = GetGearBonus("SPELLTOHIT") + local talentHit = GetTalentBonus("spellHit") + return gearHit + talentHit, gearHit, talentHit +end + +-- Override Safe* to use Full* when API fails +local _origSafeGetMeleeCrit = SafeGetMeleeCrit +SafeGetMeleeCrit = function() + local v = TryAPIs({ "GetCritChance", "GetMeleeCritChance", "GetPlayerCritChance" }) + if v > 0 then return v end + v = TryCombatRating(_G.CR_CRIT_MELEE or 9) + if v > 0 then return v end + local total = FullMeleeCrit() + return total +end +SafeGetRangedCrit = function() + local v = TryAPIs({ "GetRangedCritChance" }) + if v > 0 then return v end + v = TryCombatRating(_G.CR_CRIT_RANGED or 10) + if v > 0 then return v end + local total = FullRangedCrit() + return total +end +SafeGetSpellCrit = function() + local v = TryAPIsArg({ "GetSpellCritChance" }, 2) + if v > 0 then return v end + v = TryCombatRating(_G.CR_CRIT_SPELL or 11) + if v > 0 then return v end + local total = FullSpellCrit() + return total +end +SafeGetMeleeHit = function() + local v = TryAPIs({ "GetHitModifier", "GetMeleeHitModifier", "GetCombatMissChance" }) + if v > 0 then return v end + v = TryCombatRating(_G.CR_HIT_MELEE or 6) + if v > 0 then return v end + local total = FullMeleeHit() + return total +end +SafeGetSpellHit = function() + local v = TryAPIs({ "GetSpellHitModifier" }) + if v > 0 then return v end + v = TryCombatRating(_G.CR_HIT_SPELL or 8) + if v > 0 then return v end + local total = FullSpellHit() + return total +end + +-------------------------------------------------------------------------------- +-- CS: Combat Stats utility table (single upvalue for all tooltip helpers, +-- avoids Lua 5.0's 32-upvalue-per-closure limit) +-------------------------------------------------------------------------------- +local CS = {} +function CS.TipKV(key, val, kr, kg, kb, vr, vg, vb) + GameTooltip:AddDoubleLine(key, val, + kr or 0.7, kg or 0.7, kb or 0.75, + vr or 1, vg or 1, vb or 1) +end +function CS.TipLine(txt, r, g, b) + GameTooltip:AddLine(txt, r or 0.65, g or 0.65, b or 0.7, 1) +end +function CS.CalcArmorReduction(armor, level) + local lvl = level or UnitLevel("player") or 60 + local k = 400 + 85 * lvl + local pct = armor / (armor + k) * 100 + if pct < 0 then pct = 0 end + if pct > 75 then pct = 75 end + return pct +end +function CS.StatBaseBonus(idx) + local base, eff = UnitStat("player", idx) + base = math.floor(base or 0) + eff = math.floor(eff or 0) + return base, eff, eff - base +end +function CS.TipStatValues(base, eff, bonus) + CS.TipKV("基础值:", tostring(base)) + if bonus ~= 0 then + CS.TipKV("装备/Buff 加成:", string.format("%+d", bonus), + 0.7,0.7,0.75, bonus > 0 and 0 or 1, bonus > 0 and 1 or 0, 0) + end + CS.TipKV("当前合计:", tostring(eff), 0.7,0.7,0.75, 1,0.9,0.6) +end +CS.SafeGetMeleeCrit = SafeGetMeleeCrit +CS.SafeGetRangedCrit = SafeGetRangedCrit +CS.SafeGetSpellCrit = SafeGetSpellCrit +CS.SafeGetMeleeHit = SafeGetMeleeHit +CS.SafeGetSpellHit = SafeGetSpellHit +CS.IsCritFromAPI = IsCritFromAPI +CS.TryAPIs = TryAPIs +CS.TryAPIsArg = TryAPIsArg +CS.FullMeleeCrit = FullMeleeCrit +CS.FullRangedCrit = FullRangedCrit +CS.FullSpellCrit = FullSpellCrit +CS.FullMeleeHit = FullMeleeHit +CS.FullSpellHit = FullSpellHit +CS.GetTalentDetailsFor = GetTalentDetailsFor +CS.GetGearBonus = GetGearBonus +CS.GetItemBonusLib = GetItemBonusLib +CS.AGI_PER_MELEE_CRIT = AGI_PER_MELEE_CRIT + +SFrames.CharacterPanel.CS = CS + +local function GetItemQualityFromLink(link) + if not link then return nil end + local _, _, q = string.find(link, "|c(%x+)|H") + if not q then return nil end + local m = { + ["ff9d9d9d"] = 0, ["ffffffff"] = 1, ["ff1eff00"] = 2, + ["ff0070dd"] = 3, ["ffa335ee"] = 4, ["ffff8000"] = 5, + } + return m[q] +end + +-------------------------------------------------------------------------------- +-- Scroll helper +-------------------------------------------------------------------------------- +local function CreateScrollFrame(parent, width, height) + local holder = CreateFrame("Frame", NextName("SH"), parent) + holder:SetWidth(width) + holder:SetHeight(height) + + local scroll = CreateFrame("ScrollFrame", NextName("SF"), holder) + scroll:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0) + scroll:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -10, 0) + + local child = CreateFrame("Frame", NextName("SC"), scroll) + child:SetWidth(width - 14) + child:SetHeight(1) + scroll:SetScrollChild(child) + + local slider = CreateFrame("Slider", NextName("SB"), holder) + slider:SetWidth(6) + slider:SetPoint("TOPRIGHT", holder, "TOPRIGHT", -1, -2) + slider:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -1, 2) + slider:SetOrientation("VERTICAL") + slider:SetMinMaxValues(0, 1) + slider:SetValue(0) + SetPixelBackdrop(slider, { 0.1, 0.1, 0.12, 0.4 }, { 0.15, 0.15, 0.18, 0.3 }) + + local thumb = slider:CreateTexture(nil, "OVERLAY") + thumb:SetTexture("Interface\\Buttons\\WHITE8X8") + thumb:SetVertexColor(0.4, 0.4, 0.48, 0.7) + thumb:SetWidth(6) + thumb:SetHeight(28) + slider:SetThumbTexture(thumb) + + slider:SetScript("OnValueChanged", function() + scroll:SetVerticalScroll(this:GetValue()) + end) + scroll:EnableMouseWheel(1) + scroll:SetScript("OnMouseWheel", function() + local cur = slider:GetValue() + local step = 28 + local _, mx = slider:GetMinMaxValues() + if arg1 > 0 then + slider:SetValue(math.max(cur - step, 0)) + else + slider:SetValue(math.min(cur + step, mx)) + end + end) + + holder.scroll = scroll + holder.child = child + holder.slider = slider + holder.SetContentHeight = function(self, h) + child:SetHeight(h) + local visH = scroll:GetHeight() + local maxS = math.max(h - visH, 0) + slider:SetMinMaxValues(0, maxS) + if maxS == 0 then slider:Hide() else slider:Show() end + slider:SetValue(math.min(slider:GetValue(), maxS)) + end + return holder +end + +-------------------------------------------------------------------------------- +-- Main Frame +-------------------------------------------------------------------------------- +local function CreateMainFrame() + if panel then return panel end + + local f = CreateFrame("Frame", "SFramesCharacterPanel", UIParent) + f:SetWidth(FRAME_W) + f:SetHeight(FRAME_H) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + f:SetFrameStrata("HIGH") + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + f:SetClampedToScreen(true) + SetRoundBackdrop(f, T.bg, T.border) + CreateShadow(f, 4) + f:Hide() + + table.insert(UISpecialFrames, "SFramesCharacterPanel") + + f:SetScript("OnHide", function() + if SFrames.StatSummary and SFrames.StatSummary.Hide then + SFrames.StatSummary:Hide() + end + end) + + -- Title bar: Class icon + Name + subtitle + title button + close + f.classIcon = SFrames:CreateClassIcon(f, 16) + f.classIcon.overlay:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD + 2, -6) + + f.titleText = MakeFS(f, 12, "LEFT", T.gold) + f.titleText:SetPoint("LEFT", f.classIcon.overlay, "RIGHT", 4, 0) + + f.subtitleText = MakeFS(f, 9, "LEFT", T.dimText) + f.subtitleText:SetPoint("LEFT", f.titleText, "RIGHT", 6, 0) + + -- Weight level display (hoverable, shows iLvl in tooltip) + local weightBtn = CreateFrame("Button", nil, f) + weightBtn:SetHeight(14) + weightBtn:SetWidth(80) + weightBtn:SetPoint("LEFT", f.titleText, "RIGHT", 4, 0) + weightBtn:SetFrameLevel(f:GetFrameLevel() + 3) + local weightText = MakeFS(weightBtn, 9, "LEFT", { 0.55, 0.95, 0.55 }) + weightText:SetPoint("LEFT", weightBtn, "LEFT", 0, 0) + weightText:SetText("") + weightBtn.text = weightText + weightBtn.cachedAvgIlvl = nil + weightBtn.cachedAvgEP = nil + weightBtn.cachedClass = nil + weightBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_BOTTOM") + GameTooltip:AddLine("装备权重等级", 0.55, 0.95, 0.55) + if this.cachedAvgEP then + GameTooltip:AddDoubleLine("权重等级 (EP)", string.format("%.1f", this.cachedAvgEP), 0.8, 0.8, 0.8, 1, 1, 1) + end + if this.cachedAvgIlvl then + GameTooltip:AddDoubleLine("平均装等 (iLvl)", string.format("%.1f", this.cachedAvgIlvl), 0.8, 0.8, 0.8, 1, 0.82, 0) + end + if this.cachedClass then + local classNames = { + WARRIOR = "战士", ROGUE = "盗贼", HUNTER = "猎人", + DRUID = "德鲁伊", PALADIN = "圣骑士", SHAMAN = "萨满祭司", + MAGE = "法师", WARLOCK = "术士", PRIEST = "牧师", + } + GameTooltip:AddDoubleLine("权重模板", classNames[this.cachedClass] or this.cachedClass, 0.8, 0.8, 0.8, 0.6, 0.8, 1) + end + GameTooltip:AddLine(" ") + GameTooltip:AddLine("基于职业属性EP权重计算", 0.5, 0.5, 0.5) + GameTooltip:Show() + end) + weightBtn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + f.weightBtn = weightBtn + + -- Stat summary toggle button + local statBtn = CreateFrame("Button", nil, f) + statBtn:SetWidth(14) + statBtn:SetHeight(14) + statBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -42, -7) + statBtn:SetFrameLevel(f:GetFrameLevel() + 3) + SetPixelBackdrop(statBtn, T.btnBg, T.btnBorder) + local stTxt = MakeFS(statBtn, 9, "CENTER", T.dimText) + stTxt:SetPoint("CENTER", statBtn, "CENTER", 0, 0) + stTxt:SetText("S") + statBtn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHover[1], T.btnHover[2], T.btnHover[3], T.btnHover[4]) + GameTooltip:SetOwner(this, "ANCHOR_BOTTOM") + GameTooltip:AddLine("属性总览/装备附魔", 1, 1, 1) + GameTooltip:Show() + end) + statBtn:SetScript("OnLeave", function() + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + GameTooltip:Hide() + end) + statBtn:SetScript("OnClick", function() + if SFrames.StatSummary and SFrames.StatSummary.Toggle then + SFrames.StatSummary:Toggle() + else + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] StatSummary module not loaded!|r") + end + end) + f.statBtn = statBtn + + -- Title selection button (small, next to subtitle) + local titleBtn = CreateFrame("Button", nil, f) + titleBtn:SetWidth(14) + titleBtn:SetHeight(14) + titleBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -26, -7) + titleBtn:SetFrameLevel(f:GetFrameLevel() + 3) + SetPixelBackdrop(titleBtn, T.btnBg, T.btnBorder) + local tbTxt = MakeFS(titleBtn, 9, "CENTER", T.dimText) + tbTxt:SetPoint("CENTER", titleBtn, "CENTER", 0, 0) + tbTxt:SetText("T") + titleBtn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHover[1], T.btnHover[2], T.btnHover[3], T.btnHover[4]) + GameTooltip:SetOwner(this, "ANCHOR_BOTTOM") + GameTooltip:AddLine("选择称号", 1, 1, 1) + GameTooltip:Show() + end) + titleBtn:SetScript("OnLeave", function() + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + GameTooltip:Hide() + end) + titleBtn:SetScript("OnClick", function() + CP:ToggleTitlePopup() + end) + f.titleBtn = titleBtn + + -- Close button (round) + local closeBtn = CreateFrame("Button", nil, f) + closeBtn:SetWidth(16) + closeBtn:SetHeight(16) + closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -7, -6) + closeBtn:SetFrameLevel(f:GetFrameLevel() + 3) + SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder) + local closeIco = SFrames:CreateIcon(closeBtn, "close", 10) + closeIco:SetDrawLayer("OVERLAY") + closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) + closeIco:SetVertexColor(1, 0.7, 0.7) + closeBtn:SetScript("OnClick", function() f:Hide() end) + closeBtn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + end) + closeBtn:SetScript("OnLeave", function() + this:SetBackdropColor(T.buttonDownBg[1], T.buttonDownBg[2], T.buttonDownBg[3], T.buttonDownBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + end) + + MakeSep(f, 6, -HEADER_H, -6, -HEADER_H) + + -- Tab bar + tabs = {} + pages = {} + local numTabs = table.getn(TAB_NAMES) + local gap = 2 + local tabW = (FRAME_W - SIDE_PAD * 2 - (numTabs - 1) * gap) / numTabs + + for i, name in ipairs(TAB_NAMES) do + local btn = CreateFrame("Button", NextName("Tab"), f) + btn:SetWidth(tabW) + btn:SetHeight(TAB_H) + btn:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD + (i - 1) * (tabW + gap), -(HEADER_H + 3)) + btn:SetFrameLevel(f:GetFrameLevel() + 2) + SetRoundBackdrop(btn, T.tabBg, T.tabBorder) + + local txt = MakeFS(btn, 9, "CENTER", T.tabText) + txt:SetPoint("CENTER", btn, "CENTER", 0, 0) + txt:SetText(name) + btn.label = txt + btn.tabIndex = i + + btn:SetScript("OnClick", function() CP:SetTab(this.tabIndex) end) + btn:SetScript("OnEnter", function() + if this.tabIndex ~= CP.activeTab then + this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], 0.6) + end + end) + btn:SetScript("OnLeave", function() + if this.tabIndex ~= CP.activeTab then + this:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) + end + end) + + tabs[i] = btn + + local page = CreateFrame("Frame", NextName("Page"), f) + page:SetPoint("TOPLEFT", f, "TOPLEFT", 4, CONTENT_TOP) + page:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 4) + page:Hide() + pages[i] = page + end + + MakeSep(f, 6, CONTENT_TOP, -6, CONTENT_TOP) + + panel = f + return f +end + +function CP:SetTab(index) + self.activeTab = index + for i = 1, table.getn(TAB_NAMES) do + if i == index then + tabs[i]:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) + tabs[i]:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) + tabs[i].label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + pages[i]:Show() + else + tabs[i]:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) + tabs[i]:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], T.tabBorder[4]) + tabs[i].label:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + pages[i]:Hide() + end + end + self:UpdateCurrentTab() +end + +function CP:UpdateTitle() + if not panel then return end + local name = UnitName("player") or "" + local level = UnitLevel("player") or 0 + local classLocal, classEn = UnitClass("player") + classLocal = classLocal or "" + classEn = classEn or "" + local raceLocal = UnitRace("player") or "" + + local cc = SFrames.Config and SFrames.Config.colors and SFrames.Config.colors.class and SFrames.Config.colors.class[classEn] + + local selectedTitle = SFramesDB and SFramesDB.charSelectedTitle or nil + local titlePrefix = "" + if selectedTitle and selectedTitle ~= "" then + titlePrefix = selectedTitle .. " " + elseif selectedTitle == nil then + if GetPVPRankInfo and UnitPVPRank then + local rankName = GetPVPRankInfo(UnitPVPRank("player")) + if rankName and rankName ~= "" then + titlePrefix = rankName .. " " + end + end + end + + local fullName = titlePrefix .. name + + if pages and pages[1] and pages[1].modelNameText then + if cc then + pages[1].modelNameText:SetTextColor(cc.r, cc.g, cc.b) + else + pages[1].modelNameText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + end + pages[1].modelNameText:SetText(fullName) + end + + if pages and pages[1] and pages[1].modelGuildText then + local guildName = (SFramesDB and SFramesDB.charShowGuild ~= false) and GetGuildInfo and GetGuildInfo("player") or nil + if guildName and guildName ~= "" then + pages[1].modelGuildText:SetText("<" .. guildName .. ">") + else + pages[1].modelGuildText:SetText("") + end + end + + local parts = {} + table.insert(parts, "Lv." .. level) + table.insert(parts, raceLocal .. classLocal) + + local scoreSlots = { 1,2,3,5,6,7,8,9,10,11,12,13,14,15,16,17,18 } + local totalIlvl, ilvlCount = 0, 0 + local totalEP, epCount = 0, 0 + local showIlvl = (not SFramesDB or SFramesDB.showItemLevel ~= false) and LibItem_Level + + for _, sid in ipairs(scoreSlots) do + local link = GetInventoryItemLink("player", sid) + if link then + if showIlvl then + local _, _, itemId = string.find(link, "item:(%d+)") + local ilvl = itemId and LibItem_Level[tonumber(itemId)] + if ilvl and ilvl > 0 then + totalIlvl = totalIlvl + ilvl + ilvlCount = ilvlCount + 1 + end + end + local ep = CalcItemEP(link, classEn) + if ep > 0 then + totalEP = totalEP + ep + epCount = epCount + 1 + end + end + end + + local avgIlvl = ilvlCount > 0 and (totalIlvl / ilvlCount) or nil + local avgEP = epCount > 0 and (totalEP / epCount) or nil + + if panel.weightBtn then + panel.weightBtn.cachedAvgIlvl = avgIlvl + panel.weightBtn.cachedAvgEP = avgEP + panel.weightBtn.cachedClass = classEn + if avgEP then + panel.weightBtn.text:SetText(string.format("EP:%.1f", avgEP)) + panel.weightBtn:SetWidth(panel.weightBtn.text:GetStringWidth() + 4) + panel.weightBtn:Show() + elseif avgIlvl then + panel.weightBtn.text:SetText(string.format("iLvl:%.1f", avgIlvl)) + panel.weightBtn:SetWidth(panel.weightBtn.text:GetStringWidth() + 4) + panel.weightBtn:Show() + else + panel.weightBtn.text:SetText("") + panel.weightBtn:Hide() + end + end + + if cc then + panel.titleText:SetTextColor(cc.r, cc.g, cc.b) + else + panel.titleText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + end + panel.titleText:SetText(table.concat(parts, " ")) + panel.subtitleText:SetText("") + + if panel.classIcon then + SFrames:SetClassIcon(panel.classIcon, classEn) + end +end + +function CP:UpdateCurrentTab() + if not panel or not panel:IsShown() then return end + self:UpdateTitle() + local tab = self.activeTab or 1 + if tab == 1 then self:UpdateEquipment() + elseif tab == 2 then self:UpdateReputation() + elseif tab == 3 then self:UpdateSkills() + elseif tab == 4 then self:UpdateHonor() + end +end + +function CP:Toggle(tab) + CreateMainFrame() + self:BuildAllPages() + if panel:IsShown() then + if tab and self.activeTab ~= tab then self:SetTab(tab) + else panel:Hide() end + else + local scale = SFramesDB and SFramesDB.charPanelScale or 1.0 + panel:SetScale(scale) + panel:Show() + self:SetTab(tab or self.activeTab or 1) + if SFrames.StatSummary and SFrames.StatSummary.Show then + SFrames.StatSummary:Show() + end + end +end + +function CP:Show(tab) + CreateMainFrame() + self:BuildAllPages() + panel:Show() + self:SetTab(tab or self.activeTab or 1) + if SFrames.StatSummary and SFrames.StatSummary.Show then + SFrames.StatSummary:Show() + end +end + +function CP:Hide() + if panel then panel:Hide() end +end + +-------------------------------------------------------------------------------- +-- Title selection popup +-------------------------------------------------------------------------------- +local titlePopup + +function CP:ToggleTitlePopup() + if titlePopup and titlePopup:IsShown() then + titlePopup:Hide() + return + end + self:ShowTitlePopup() +end + +function CP:ShowTitlePopup() + if not panel then return end + + if not titlePopup then + titlePopup = CreateFrame("Frame", "SFramesCPTitlePopup", panel) + titlePopup:SetWidth(160) + titlePopup:SetFrameStrata("TOOLTIP") + titlePopup:SetFrameLevel(200) + SetRoundBackdrop(titlePopup, T.bg, T.border) + titlePopup:Hide() + titlePopup:EnableMouse(true) + end + + -- Clear old children + if titlePopup.rows then + for _, row in ipairs(titlePopup.rows) do + if row.frame then row.frame:Hide() end + end + end + titlePopup.rows = {} + + -- Build title list from all available sources + local titles = {} + local seen = {} + table.insert(titles, { name = "无称号", key = "" }) + + -- PVP rank title + if GetPVPRankInfo and UnitPVPRank then + local rank = UnitPVPRank("player") + if rank and rank > 0 then + local rankName = GetPVPRankInfo(rank) + if rankName and rankName ~= "" and not seen[rankName] then + table.insert(titles, { name = rankName, key = rankName }) + seen[rankName] = true + end + end + end + + -- Turtle WoW custom titles: GetNumTitles / GetTitleName / IsTitleKnown + if GetNumTitles and GetTitleName then + local numT = GetNumTitles() + if numT and numT > 0 then + for ti = 1, numT do + local tName = GetTitleName(ti) + if tName and tName ~= "" and not seen[tName] then + local known = true + if IsTitleKnown then known = IsTitleKnown(ti) end + if known then + table.insert(titles, { name = tName, key = tName, titleId = ti }) + seen[tName] = true + end + end + end + end + end + + -- Fallback: scan CharacterTitleText if it exists (some private servers) + if table.getn(titles) <= 1 and CharacterTitleText then + local curTitle = CharacterTitleText:GetText() + if curTitle and curTitle ~= "" and not seen[curTitle] then + table.insert(titles, { name = curTitle, key = curTitle }) + seen[curTitle] = true + end + end + + local currentTitle = SFramesDB and SFramesDB.charSelectedTitle or "" + local rowH = 20 + local y = -4 + local popH = 8 + + for i, t in ipairs(titles) do + local row = CreateFrame("Button", nil, titlePopup) + row:SetWidth(152) + row:SetHeight(rowH) + row:SetPoint("TOPLEFT", titlePopup, "TOPLEFT", 4, y) + + local isActive = (currentTitle == t.key) or (currentTitle == "" and t.key == "") + local bg = isActive and T.tabActiveBg or T.tabBg + local bd = isActive and T.tabActiveBorder or T.tabBorder + SetPixelBackdrop(row, bg, bd) + + local txt = MakeFS(row, 10, "LEFT", isActive and T.tabActiveText or T.valueText) + txt:SetPoint("LEFT", row, "LEFT", 6, 0) + txt:SetText(t.name) + + if isActive then + local mark = MakeFS(row, 9, "RIGHT", T.gold) + mark:SetPoint("RIGHT", row, "RIGHT", -6, 0) + mark:SetText("*") + end + + row.titleKey = t.key + row.titleId = t.titleId + row:SetScript("OnClick", function() + SFramesDB = SFramesDB or {} + SFramesDB.charSelectedTitle = this.titleKey + -- Try to set server-side title via Turtle WoW API + if SetCurrentTitle and this.titleId then + SetCurrentTitle(this.titleId) + elseif SetCurrentTitle and this.titleKey == "" then + SetCurrentTitle(0) + end + titlePopup:Hide() + CP:UpdateTitle() + end) + row:SetScript("OnEnter", function() + if not isActive then + this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], 0.5) + end + end) + row:SetScript("OnLeave", function() + if not isActive then + this:SetBackdropColor(bg[1], bg[2], bg[3], bg[4]) + end + end) + + table.insert(titlePopup.rows, { frame = row }) + y = y - rowH - 1 + popH = popH + rowH + 1 + end + + titlePopup:SetHeight(popH) + titlePopup:ClearAllPoints() + titlePopup:SetPoint("TOPRIGHT", panel.titleBtn, "BOTTOMRIGHT", 0, -2) + titlePopup:Show() +end + +function CP:BuildAllPages() + if self.built then return end + self.built = true + self:BuildEquipmentPage() + self:BuildReputationPage() + self:BuildSkillsPage() + self:BuildHonorPage() +end + +-------------------------------------------------------------------------------- +-- Stat helpers (shared by equipment page and others) +-------------------------------------------------------------------------------- +function CP:CreateStatSection(parent, title, yOffset) + local header = MakeFS(parent, 11, "LEFT", T.sectionTitle) + header:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, yOffset) + header:SetText(title) + local sep = parent:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + sep:SetHeight(1) + sep:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, yOffset - 14) + sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -8, yOffset - 14) + return yOffset - 18 +end + +function CP:CreateStatRow(parent, label, yOffset, color) + local row = {} + row.label = MakeFS(parent, 10, "LEFT", T.labelText) + row.label:SetPoint("TOPLEFT", parent, "TOPLEFT", 14, yOffset) + row.label:SetText(label) + row.value = MakeFS(parent, 10, "RIGHT", color or T.valueText) + row.value:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -14, yOffset) + row.value:SetWidth(120) + row.value:SetJustifyH("RIGHT") + return row, yOffset - 16 +end + +-------------------------------------------------------------------------------- +-- Tab 1: Equipment + Stats (merged, scrollable) +-------------------------------------------------------------------------------- +function CP:BuildEquipmentPage() + local page = pages[1] + if page.built then return end + page.built = true + + local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4 + local scrollArea = CreateScrollFrame(page, CONTENT_W, contentH) + scrollArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + page.scrollArea = scrollArea + + local child = scrollArea.child + local cw = SCROLL_W + + -- 3D Model: created on page level (not scroll child) so it follows panel drag + local slotColW = SLOT_SIZE + 6 + local modelW = cw - slotColW * 2 - 26 + if modelW < 80 then modelW = 80 end + local modelH = 8 * (SLOT_SIZE + SLOT_GAP) - SLOT_GAP + + -- Background frame in scroll child (for the dark border visual) + local modelBg = CreateFrame("Frame", nil, child) + modelBg:SetWidth(modelW) + modelBg:SetHeight(modelH) + modelBg:SetPoint("TOP", child, "TOP", 0, -4) + SetRoundBackdrop(modelBg, T.modelBg, T.modelBorder) + page.modelBgFrame = modelBg + + -- PlayerModel on page level, anchored to the background frame + local modelFrame = CreateFrame("Frame", nil, page) + modelFrame:SetWidth(modelW - 8) + modelFrame:SetHeight(modelH - 24) + modelFrame:SetPoint("TOP", modelBg, "TOP", 0, -4) + modelFrame:SetFrameLevel(page:GetFrameLevel() + 5) + + local model = CreateFrame("PlayerModel", NextName("Model"), modelFrame) + model:SetAllPoints(modelFrame) + page.model = model + page.modelFrame = modelFrame + + model:EnableMouse(true) + model:EnableMouseWheel(1) + model.rotating = false + model.panning = false + model.curFacing = 0.4 + model.curScale = 1.0 + model.posX = 0 + model.posY = 0 + model.posZ = 0 + + model:SetScript("OnMouseDown", function() + if arg1 == "LeftButton" then + this.rotating = true + this.startX = GetCursorPosition() + this.startFacing = this.curFacing or 0 + elseif arg1 == "RightButton" then + this.panning = true + local cx, cy = GetCursorPosition() + this.panStartX = cx + this.panStartY = cy + this.panOriginX = this.posX or 0 + this.panOriginY = this.posY or 0 + end + end) + model:SetScript("OnMouseUp", function() + if arg1 == "LeftButton" then + this.rotating = false + elseif arg1 == "RightButton" then + this.panning = false + end + end) + model:SetScript("OnMouseWheel", function() + local step = 0.1 + local ns = (this.curScale or 1) + arg1 * step + if ns < 0.2 then ns = 0.2 end + if ns > 4.0 then ns = 4.0 end + this.curScale = ns + this:SetModelScale(ns) + end) + model:SetScript("OnUpdate", function() + if this.rotating then + local cx = GetCursorPosition() + local diff = (cx - (this.startX or cx)) * 0.01 + local nf = (this.startFacing or 0) + diff + this.curFacing = nf + this:SetFacing(nf) + elseif this.panning then + local cx, cy = GetCursorPosition() + local es = this:GetEffectiveScale() + if es < 0.01 then es = 1 end + local dx = (cx - (this.panStartX or cx)) / (es * 35) + local dy = (cy - (this.panStartY or cy)) / (es * 35) + this.posX = (this.panOriginX or 0) + dx + this.posY = (this.panOriginY or 0) + dy + this:SetPosition(this.posY, 0, this.posX) + elseif this.autoRotateDir then + local speed = 1.5 * (arg1 or 0.016) + this.curFacing = (this.curFacing or 0) + speed * this.autoRotateDir + this:SetFacing(this.curFacing) + end + end) + model.ResetView = function(self) + self.curFacing = 0.4 + self.curScale = 1.0 + self.posX = 0 + self.posY = 0 + self:SetFacing(0.4) + self:SetModelScale(1.0) + self:SetPosition(0, 0, 0) + end + + -- Name/guild overlay floats on top of 3D model + local nameOverlay = CreateFrame("Frame", nil, page) + nameOverlay:SetWidth(modelW) + nameOverlay:SetHeight(30) + nameOverlay:SetPoint("TOP", modelBg, "TOP", 0, -4) + nameOverlay:SetFrameLevel(page:GetFrameLevel() + 7) + nameOverlay:EnableMouse(false) + + local modelNameText = nameOverlay:CreateFontString(nil, "OVERLAY") + modelNameText:SetFont(GetFont(), 11, "OUTLINE") + modelNameText:SetPoint("TOP", nameOverlay, "TOP", 0, -5) + modelNameText:SetJustifyH("CENTER") + modelNameText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + page.modelNameText = modelNameText + + local modelGuildText = nameOverlay:CreateFontString(nil, "OVERLAY") + modelGuildText:SetFont(GetFont(), 9, "OUTLINE") + modelGuildText:SetPoint("TOP", modelNameText, "BOTTOM", 0, -1) + modelGuildText:SetJustifyH("CENTER") + modelGuildText:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + page.modelGuildText = modelGuildText + + -- Model toolbar: unified strip with 3 segments + local tbH = 14 + local segW = 24 + local tbW = segW * 3 + 2 + local toolbar = CreateFrame("Frame", nil, page) + toolbar:SetWidth(tbW) + toolbar:SetHeight(tbH) + toolbar:SetPoint("BOTTOM", modelBg, "BOTTOM", 0, 5) + toolbar:SetFrameLevel(page:GetFrameLevel() + 6) + SetPixelBackdrop(toolbar, { 0.04, 0.04, 0.06, 0.75 }, { 0.22, 0.22, 0.28, 0.5 }) + page.modelToolbar = toolbar + + local function MakeToolBtn(parent, w, text, anchor, ox) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w) + btn:SetHeight(tbH - 2) + btn:SetPoint("LEFT", parent, "LEFT", ox, 0) + btn:SetFrameLevel(parent:GetFrameLevel() + 1) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 0, + }) + btn:SetBackdropColor(0, 0, 0, 0) + local fs = MakeFS(btn, 9, "CENTER", { 0.5, 0.5, 0.55 }) + fs:SetPoint("CENTER", btn, "CENTER", 0, 0) + fs:SetText(text) + btn.label = fs + btn:SetScript("OnEnter", function() + this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) + this.label:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + end) + btn:SetScript("OnLeave", function() + this:SetBackdropColor(0, 0, 0, 0) + this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + end) + return btn + end + + local rotLeft = MakeToolBtn(toolbar, segW, "<", "LEFT", 1) + rotLeft:SetScript("OnMouseDown", function() + model.autoRotateDir = 1 + this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4] or 0.6) + end) + rotLeft:SetScript("OnMouseUp", function() + model.autoRotateDir = nil + this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) + end) + rotLeft:SetScript("OnLeave", function() + this:SetBackdropColor(0, 0, 0, 0) + this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + model.autoRotateDir = nil + end) + page.rotLeft = rotLeft + + local sep1 = toolbar:CreateTexture(nil, "OVERLAY") + sep1:SetTexture("Interface\\Buttons\\WHITE8X8") + sep1:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4] or 0.4) + sep1:SetWidth(1) + sep1:SetHeight(tbH - 4) + sep1:SetPoint("LEFT", toolbar, "LEFT", segW + 1, 0) + + local resetBtn = MakeToolBtn(toolbar, segW, "O", "LEFT", segW + 1) + resetBtn:SetScript("OnClick", function() model:ResetView() end) + resetBtn:SetScript("OnEnter", function() + this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) + this.label:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + GameTooltip:SetOwner(this, "ANCHOR_BOTTOM") + GameTooltip:AddLine("重置视角", 1, 1, 1) + GameTooltip:Show() + end) + resetBtn:SetScript("OnLeave", function() + this:SetBackdropColor(0, 0, 0, 0) + this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + GameTooltip:Hide() + end) + page.resetBtn = resetBtn + + local sep2 = toolbar:CreateTexture(nil, "OVERLAY") + sep2:SetTexture("Interface\\Buttons\\WHITE8X8") + sep2:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4] or 0.4) + sep2:SetWidth(1) + sep2:SetHeight(tbH - 4) + sep2:SetPoint("LEFT", toolbar, "LEFT", segW * 2 + 1, 0) + + local rotRight = MakeToolBtn(toolbar, segW, ">", "LEFT", segW * 2 + 1) + rotRight:SetScript("OnMouseDown", function() + model.autoRotateDir = -1 + this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4] or 0.6) + end) + rotRight:SetScript("OnMouseUp", function() + model.autoRotateDir = nil + this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) + end) + rotRight:SetScript("OnLeave", function() + this:SetBackdropColor(0, 0, 0, 0) + this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + model.autoRotateDir = nil + end) + page.rotRight = rotRight + + -- Equipment slots + local slotY = -6 + page.equipSlots = {} + + for idx, si in ipairs(EQUIP_SLOTS_LEFT) do + local slot = self:CreateEquipSlot(child, si.id, si.name) + slot:SetPoint("TOPLEFT", child, "TOPLEFT", 2, slotY - (idx - 1) * (SLOT_SIZE + SLOT_GAP)) + page.equipSlots[si.id] = slot + end + + for idx, si in ipairs(EQUIP_SLOTS_RIGHT) do + local slot = self:CreateEquipSlot(child, si.id, si.name) + slot:SetPoint("TOPRIGHT", child, "TOPRIGHT", -2, slotY - (idx - 1) * (SLOT_SIZE + SLOT_GAP)) + page.equipSlots[si.id] = slot + end + + local bottomY = slotY - table.getn(EQUIP_SLOTS_LEFT) * (SLOT_SIZE + SLOT_GAP) - 4 + local bottomCount = table.getn(EQUIP_SLOTS_BOTTOM) + local totalBW = bottomCount * SLOT_SIZE + (bottomCount - 1) * (SLOT_GAP + 2) + local bsx = math.max((cw - totalBW) / 2, 4) + + for idx, si in ipairs(EQUIP_SLOTS_BOTTOM) do + local slot = self:CreateEquipSlot(child, si.id, si.name) + slot:SetPoint("TOPLEFT", child, "TOPLEFT", bsx + (idx - 1) * (SLOT_SIZE + SLOT_GAP + 1), bottomY) + page.equipSlots[si.id] = slot + end + + local infoY = bottomY - SLOT_SIZE + + -- Stats panel: fixed at bottom of page (not in scroll child) + -- This avoids scroll/overflow issues + local STAT_PANEL_H = 100 + local ROW_H = 14 + + local statPanel = CreateFrame("Frame", nil, page) + statPanel:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0) + statPanel:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) + statPanel:SetHeight(STAT_PANEL_H) + statPanel:SetFrameLevel(page:GetFrameLevel() + 2) + page.statPanel = statPanel + + -- Resize scroll area to not overlap stat panel + scrollArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, STAT_PANEL_H) + + MakeSep(statPanel, 4, 0, -4, 0) + + -- Category buttons + local catNames = { "基础/抗性", "攻击", "法术/防御" } + local catBtnW = math.floor((CONTENT_W - 12) / 3) + page.statCatBtns = {} + page.statCatFrames = {} + page.activeStatCat = 1 + + for ci = 1, 3 do + local cb = CreateFrame("Button", NextName("SC"), statPanel) + cb:SetWidth(catBtnW) + cb:SetHeight(16) + cb:SetPoint("TOPLEFT", statPanel, "TOPLEFT", 4 + (ci - 1) * (catBtnW + 2), -4) + SetPixelBackdrop(cb, T.tabBg, T.tabBorder) + local cbt = MakeFS(cb, 8, "CENTER", T.tabText) + cbt:SetPoint("CENTER", cb, "CENTER", 0, 0) + cbt:SetText(catNames[ci]) + cb.label = cbt + cb.catIndex = ci + cb:SetScript("OnClick", function() + page.activeStatCat = this.catIndex + CP:RefreshStatCatVisual(page) + CP:RefreshStatValues(page) + end) + cb:SetScript("OnEnter", function() + if this.catIndex ~= page.activeStatCat then + this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], 0.5) + end + end) + cb:SetScript("OnLeave", function() + if this.catIndex ~= page.activeStatCat then + this:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) + end + end) + page.statCatBtns[ci] = cb + end + + local catContentY = -26 + + local function MakeStatHitArea(parent, anchorFrom, offX, yOff, isRight) + local btn = CreateFrame("Button", nil, parent) + btn:SetHeight(ROW_H) + btn:SetFrameLevel(parent:GetFrameLevel() + 4) + if isRight then + btn:SetPoint("TOPLEFT", parent, "TOP", offX, yOff) + btn:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4, yOff) + else + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", 4, yOff) + btn:SetPoint("TOPRIGHT", parent, "TOP", -4, yOff) + end + if btn.SetBackdrop then btn:SetBackdrop(nil) end + if btn.SetNormalTexture then btn:SetNormalTexture(nil) end + if btn.SetHighlightTexture then btn:SetHighlightTexture(nil) end + if btn.SetPushedTexture then btn:SetPushedTexture(nil) end + return btn + end + + local function Make2ColRow(parent, labelL, labelR, yOff, colorL, colorR) + local r = {} + if labelL then + r.labelL = MakeFS(parent, 9, "LEFT", T.labelText) + r.labelL:SetPoint("TOPLEFT", parent, "TOPLEFT", 6, yOff) + r.labelL:SetText(labelL) + r.valueL = MakeFS(parent, 9, "RIGHT", colorL or T.valueText) + r.valueL:SetPoint("TOPLEFT", parent, "TOPLEFT", 6, yOff) + r.valueL:SetPoint("TOPRIGHT", parent, "TOP", -4, yOff) + r.valueL:SetJustifyH("RIGHT") + r.hitL = MakeStatHitArea(parent, "TOPLEFT", 0, yOff, false) + end + if labelR then + r.labelR = MakeFS(parent, 9, "LEFT", T.labelText) + r.labelR:SetPoint("TOPLEFT", parent, "TOP", 6, yOff) + r.labelR:SetText(labelR) + r.valueR = MakeFS(parent, 9, "RIGHT", colorR or T.valueText) + r.valueR:SetPoint("TOPLEFT", parent, "TOP", 6, yOff) + r.valueR:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -6, yOff) + r.valueR:SetJustifyH("RIGHT") + r.hitR = MakeStatHitArea(parent, "TOP", 0, yOff, true) + end + return r, yOff - ROW_H + end + + -- Cat 1: Base + Resist + local cat1 = CreateFrame("Frame", nil, statPanel) + cat1:SetAllPoints(statPanel) + page.statCatFrames[1] = cat1 + page.cat1Rows = {} + local resistSchools = { 2, 3, 4, 5, 6 } + local resistNames = { "火焰", "自然", "冰霜", "暗影", "奥术" } + + local resistSchoolName = { [2]="火焰", [3]="自然", [4]="冰霜", [5]="暗影", [6]="奥术" } + + local statOnEnter = { + -- 力量 + function() + local base, eff, bonus = CS.StatBaseBonus(1) + GameTooltip:AddLine("力量 (Strength)", 1, 0.82, 0.0) + CS.TipLine("提升近战攻强(AP)与格挡值。") + GameTooltip:AddLine(" ") + CS.TipLine("攻击强度转化:", 0.5,0.8,1) + CS.TipLine(" 战/骑/萨/德(熊豹):1力 = 2AP") + CS.TipLine(" 猫德+野性之心:1力 = 2.4AP") + CS.TipLine(" 盗/猎/法/牧/术:1力 = 1AP") + GameTooltip:AddLine(" ") + CS.TipLine("格挡值(需装备盾牌):", 0.5,0.8,1) + CS.TipLine(" 战/骑/萨:每20点额外力量 +1 BV") + CS.TipLine(" BV = (力量-种族初始力量)/20") + GameTooltip:AddLine(" ") + CS.TipStatValues(base, eff, bonus) + end, + -- 敏捷 + function() + local base, eff, bonus = CS.StatBaseBonus(2) + local crit = CS.SafeGetMeleeCrit() + local dodge = GetDodgeChance and GetDodgeChance() or 0 + local _, effArmor = UnitArmor("player") + GameTooltip:AddLine("敏捷 (Agility)", 1, 0.82, 0.0) + CS.TipLine("提升暴击、护甲、闪避。1敏 = 2护甲。") + GameTooltip:AddLine(" ") + CS.TipLine("攻击强度转化:", 0.5,0.8,1) + CS.TipLine(" 猎人:1敏 = 2远程AP + 1近战AP") + CS.TipLine(" 盗贼:1敏 = 1近战/远程AP") + CS.TipLine(" 德鲁伊(熊/豹):1敏 = 1AP") + CS.TipLine(" 其他职业:不提供额外AP") + GameTooltip:AddLine(" ") + CS.TipLine("暴击转化(60级):", 0.5,0.8,1) + CS.TipLine(" 战/骑/萨/德:20敏 = 1%暴击") + CS.TipLine(" 盗贼:29敏 = 1%暴击(最高效)") + CS.TipLine(" 猎人:52.91敏 = 1%暴击(最严苛)") + GameTooltip:AddLine(" ") + CS.TipLine("闪避转化(60级):", 0.5,0.8,1) + CS.TipLine(" 盗贼:14.5敏 = 1%闪避(最强)") + CS.TipLine(" 战/骑/萨/德:20敏 = 1%闪避") + GameTooltip:AddLine(" ") + CS.TipStatValues(base, eff, bonus) + GameTooltip:AddLine(" ") + CS.TipKV("当前物理暴击:", string.format("%.2f%%", crit), 0.7,0.7,0.75, 1,1,0.5) + if not CS.IsCritFromAPI() and crit > 0 then + CS.TipLine("(基础+敏捷+装备+天赋,无Buff)", 0.8,0.5,0.3) + end + CS.TipKV("当前闪避率:", string.format("%.2f%%", dodge), 0.7,0.7,0.75, 1,1,0.5) + CS.TipKV("当前护甲值:", tostring(math.floor(effArmor or 0)), 0.7,0.7,0.75, 1,1,0.5) + end, + -- 耐力 + function() + local base, eff, bonus = CS.StatBaseBonus(3) + local hp = UnitHealthMax("player") or 0 + GameTooltip:AddLine("耐力 (Stamina)", 1, 0.82, 0.0) + CS.TipLine("所有职业通用的生存核心属性。") + GameTooltip:AddLine(" ") + CS.TipLine("生命值转化:", 0.5,0.8,1) + CS.TipLine(" 前20点:每1耐 = 1生命值") + CS.TipLine(" 超过20点:每1耐 = 10生命值") + GameTooltip:AddLine(" ") + CS.TipLine("特殊加成:", 0.5,0.8,1) + CS.TipLine(" 牛头人:生命值总额 ×5%") + CS.TipLine(" 术士:可通过生命分流转法力") + GameTooltip:AddLine(" ") + CS.TipStatValues(base, eff, bonus) + CS.TipKV("最大生命值:", tostring(hp), 0.7,0.7,0.75, 0.4,1,0.4) + end, + -- 智力 + function() + local base, eff, bonus = CS.StatBaseBonus(4) + local mana = UnitManaMax("player") or 0 + local scrit = CS.SafeGetSpellCrit() + GameTooltip:AddLine("智力 (Intellect)", 1, 0.82, 0.0) + CS.TipLine("增加法力值和法术暴击率。") + CS.TipLine("1智力 = 15法力值(通用)。") + GameTooltip:AddLine(" ") + CS.TipLine("法爆转化(60级):", 0.5,0.8,1) + CS.TipLine(" 圣骑:29.5智 = 1%法爆(最高效!)") + CS.TipLine(" 法师:59.5智 = 1%(基础0.2%)") + CS.TipLine(" 牧师:59.5智 = 1%(基础0.8%)") + CS.TipLine(" 萨满:59.2智 = 1%(基础2.3%)") + CS.TipLine(" 德鲁:60.0智 = 1%(基础1.8%)") + CS.TipLine(" 术士:60.6智 = 1%(基础1.7%)") + CS.TipLine(" 战士/盗贼:无法系收益") + GameTooltip:AddLine(" ") + CS.TipStatValues(base, eff, bonus) + CS.TipKV("最大法力值:", tostring(mana), 0.7,0.7,0.75, 0.4,0.7,1) + CS.TipKV("法术暴击率:", string.format("%.2f%%", scrit), 0.7,0.7,0.75, 1,1,0.5) + end, + -- 精神 + function() + local base, eff, bonus = CS.StatBaseBonus(5) + GameTooltip:AddLine("精神 (Spirit)", 1, 0.82, 0.0) + CS.TipLine("控制脱战/五秒规则外的回复速度。") + CS.TipLine("停止施法5秒后触发精神回蓝。") + GameTooltip:AddLine(" ") + CS.TipLine("法力回复(每2秒/跳):", 0.5,0.8,1) + CS.TipLine(" 牧师/法师:13 + 精神/4") + CS.TipLine(" 德/萨/骑/猎:15 + 精神/5") + CS.TipLine(" 术士(惩罚):8 + 精神/4") + GameTooltip:AddLine(" ") + CS.TipLine("各职业收益:", 0.5,0.8,1) + CS.TipLine(" 牧师(极高):冥想天赋施法中回蓝") + CS.TipLine(" 精神指引:25%精神→法强/治疗") + CS.TipLine(" 德鲁伊(核心):配合激活回蓝") + CS.TipLine(" 法师(重要):配合奥术冥想天赋") + CS.TipLine(" 萨满:MP5更稳定") + CS.TipLine(" 战士:脱战回血 = 精神/50") + CS.TipLine(" 巨魔(精神战):战斗中25%回复") + GameTooltip:AddLine(" ") + CS.TipStatValues(base, eff, bonus) + end, + } + + local cy = catContentY + for i = 1, 5 do + local row + row, cy = Make2ColRow(cat1, STAT_NAMES[i], resistNames[i], cy, nil, T.resistColors[resistSchools[i]]) + row.baseIdx = i + row.resistSchool = resistSchools[i] + if row.hitL then + local fn = statOnEnter[i] + row.hitL:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:ClearLines() + fn() + GameTooltip:Show() + end) + row.hitL:SetScript("OnLeave", function() GameTooltip:Hide() end) + end + if row.hitR then + local school = resistSchools[i] + local rname = resistNames[i] + row.hitR:SetScript("OnEnter", function() + local base, total = UnitResistance("player", school) + base = math.floor(base or 0) + total = math.floor(total or base) + local bonus = total - base + -- Resistance → damage reduction: Classic formula uses total resist / (total resist + 5*attackerLevel) + -- At player level vs same-level attacker: average ~(R/(R+5L))*0.75 full absorption chance + local lvl = UnitLevel("player") or 60 + local avgPct = total / (total + 5 * lvl) * 75 + GameTooltip:SetOwner(this, "ANCHOR_LEFT") + GameTooltip:ClearLines() + GameTooltip:AddLine(rname .. "抗性", 1, 0.82, 0.0) + CS.TipLine("减少受到的" .. rname .. "系魔法伤害。") + CS.TipLine("抗性值越高,被完全抵抗或部分减免的概率越大。") + GameTooltip:AddLine(" ") + CS.TipKV("基础抗性:", tostring(base)) + if bonus ~= 0 then + CS.TipKV("装备加成:", (bonus > 0 and "+" or "") .. tostring(bonus), + 0.7,0.7,0.75, bonus>0 and 0 or 1, bonus>0 and 1 or 0, 0) + end + CS.TipKV("当前合计:", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6) + if total > 0 then + CS.TipKV("平均减伤约:", string.format("%.1f%%", avgPct), 0.7,0.7,0.75, 1,1,0.5) + end + GameTooltip:Show() + end) + row.hitR:SetScript("OnLeave", function() GameTooltip:Hide() end) + end + table.insert(page.cat1Rows, row) + end + + -- Cat 2: Melee + Ranged + local cat2 = CreateFrame("Frame", nil, statPanel) + cat2:SetAllPoints(statPanel) + cat2:Hide() + page.statCatFrames[2] = cat2 + page.cat2Rows = {} + local meleeL = { "攻强", "伤害", "速度", "暴击", "命中" } + local meleeK = { "meleeAP", "meleeDmg", "meleeSpeed", "meleeCrit", "meleeHit" } + local rangedL = { "攻强", "伤害", "速度", "暴击", "" } + local rangedK = { "rangedAP", "rangedDmg", "rangedSpeed", "rangedCrit", nil } + + local meleeOnEnter = { + -- 近战攻强 + function() + local base, pos, neg = UnitAttackPower("player") + local total = math.floor((base or 0) + (pos or 0) + (neg or 0)) + local dps = total / 14 + local bmod = math.floor((pos or 0) + (neg or 0)) + GameTooltip:AddLine("近战攻击强度 (AP)", 1, 0.82, 0.0) + CS.TipLine("直接提升近战白字伤害。") + CS.TipLine("每 14 点攻击强度 = +1 DPS。") + GameTooltip:AddLine(" ") + CS.TipLine("来源:力量(战/骑/萨/德 1力=2AP,", 0.5,0.8,1) + CS.TipLine("盗/猎/法系 1力=1AP) + 装备/Buff", 0.5,0.8,1) + GameTooltip:AddLine(" ") + CS.TipKV("基础 AP:", tostring(math.floor(base or 0))) + if bmod ~= 0 then + CS.TipKV("装备/Buff 加成:", string.format("%+d", bmod), + 0.7,0.7,0.75, bmod > 0 and 0 or 1, bmod > 0 and 1 or 0, 0) + end + CS.TipKV("当前合计:", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6) + CS.TipKV("等效 DPS 增益:", string.format("+%.1f", dps), 0.7,0.7,0.75, 1,1,0.5) + end, + -- 近战伤害 + function() + local lo, hi = UnitDamage("player") + lo = math.floor(lo or 0); hi = math.floor(hi or 0) + local ms = UnitAttackSpeed("player") or 2 + local avgDps = ms > 0 and (lo + hi) / 2 / ms or 0 + GameTooltip:AddLine("近战伤害", 1, 0.82, 0.0) + CS.TipLine("主手武器基础伤害 + 攻击强度加成后的实际白字伤害区间。") + CS.TipLine("AP 加成 = 武器速度 × AP / 14。") + GameTooltip:AddLine(" ") + CS.TipKV("伤害区间:", lo .. " - " .. hi) + CS.TipKV("主手 DPS:", string.format("%.1f", avgDps), 0.7,0.7,0.75, 1,1,0.5) + end, + -- 近战速度 + function() + local ms, os = UnitAttackSpeed("player") + GameTooltip:AddLine("近战攻击速度", 1, 0.82, 0.0) + CS.TipLine("每次自动攻击的间隔(秒),数值越低攻击越快。") + CS.TipLine("受急速效果(如狂暴/风怒图腾)影响。") + GameTooltip:AddLine(" ") + CS.TipKV("主手速度:", string.format("%.2f 秒", ms or 0)) + if os and os > 0 then + CS.TipKV("副手速度:", string.format("%.2f 秒", os)) + CS.TipLine(" ") + CS.TipLine("副手白字伤害为主手50%。", 0.5,0.8,1) + CS.TipLine("双持白字未命中:26.4%(300技能)", 0.5,0.8,1) + CS.TipLine("315技能仍有24.0%未命中。", 0.5,0.8,1) + end + end, + -- 近战暴击 + function() + local fromAPI = CS.IsCritFromAPI() + local _, class = UnitClass("player") + local ratio = CS.AGI_PER_MELEE_CRIT[class or ""] or 0 + GameTooltip:AddLine("近战暴击率", 1, 0.82, 0.0) + CS.TipLine("近战暴击造成 200% 伤害。") + if ratio > 0 then + if ratio == math.floor(ratio) then + CS.TipLine(string.format("当前职业:每 %d 敏捷 = 1%% 暴击。", ratio)) + else + CS.TipLine(string.format("当前职业:每 %.2f 敏捷 = 1%% 暴击。", ratio)) + end + end + GameTooltip:AddLine(" ") + if fromAPI then + local crit = CS.SafeGetMeleeCrit() + CS.TipKV("当前暴击率:", string.format("%.2f%%", crit), 0.7,0.7,0.75, 1,1,0.5) + else + local total, base, agiC, gearC, talC = CS.FullMeleeCrit() + CS.TipLine("来源分项:", 0.5,0.8,1) + if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end + if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) end + if gearC > 0 then CS.TipKV(" 装备暴击:", string.format("+%d%%", gearC)) end + if talC > 0 then + CS.TipKV(" 天赋暴击:", string.format("+%d%%", talC)) + for _, d in ipairs(CS.GetTalentDetailsFor("meleeCrit")) do + CS.TipLine(string.format(" %s (%d/%d) +%d%%", + d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6) + end + end + GameTooltip:AddLine(" ") + CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5) + CS.TipLine("(Buff 暴击未计入)", 0.8,0.5,0.3) + end + end, + -- 近战命中 + function() + local fromAPI = CS.TryAPIs({ "GetHitModifier", "GetMeleeHitModifier" }) > 0 + GameTooltip:AddLine("近战命中", 1, 0.82, 0.0) + CS.TipLine("减少近战未命中概率。物理DPS最优先属性。") + CS.TipLine("乌龟服对BOSS基础未命中率 8%。") + GameTooltip:AddLine(" ") + if fromAPI then + local hit = CS.SafeGetMeleeHit() + CS.TipKV("当前命中加成:", string.format("%+d%%", hit), 0.7,0.7,0.75, 1,1,0.5) + else + local total, gearH, talH = CS.FullMeleeHit() + CS.TipLine("来源分项:", 0.5,0.8,1) + if gearH > 0 then CS.TipKV(" 装备命中:", string.format("+%d%%", gearH)) end + if talH > 0 then + CS.TipKV(" 天赋命中:", string.format("+%d%%", talH)) + for _, d in ipairs(CS.GetTalentDetailsFor("meleeHit")) do + CS.TipLine(string.format(" %s (%d/%d) +%d%%", + d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6) + end + end + GameTooltip:AddLine(" ") + CS.TipKV("合计命中:", string.format("+%d%%", total), 0.7,0.7,0.75, 1,1,0.5) + end + local totalHit = CS.SafeGetMeleeHit() + local lvl = UnitLevel("player") or 60 + if lvl >= 60 then + GameTooltip:AddLine(" ") + CS.TipLine("达标判定(对+3级BOSS):", 1, 0.82, 0.0) + CS.TipLine("已移除隐性1%命中压制。", 0.5,0.8,1) + local cap = 8 + if totalHit >= cap then + CS.TipKV(" 黄字8%(300技能):", "已达标", 0.7,0.7,0.75, 0.3,1,0.3) + else + CS.TipKV(" 黄字8%(300技能):", string.format("差%d%%", cap - totalHit), + 0.7,0.7,0.75, 1,0.3,0.3) + end + if totalHit >= 5 then + CS.TipKV(" 种族5%(305技能):", "已达标", 0.7,0.7,0.75, 0.3,1,0.3) + else + CS.TipKV(" 种族5%(305技能):", string.format("差%d%%", 5 - totalHit), + 0.7,0.7,0.75, 1,0.3,0.3) + end + if totalHit >= 2 then + CS.TipKV(" 高技能2%(310):", "已达标", 0.7,0.7,0.75, 0.3,1,0.3) + else + CS.TipKV(" 高技能2%(310):", string.format("差%d%%", 2 - totalHit), + 0.7,0.7,0.75, 1,0.3,0.3) + end + CS.TipLine(" 315技能时命中需求归零", 0.55,0.55,0.6) + CS.TipLine(" (人类剑锤/兽人斧/矮人枪锤/侏儒短剑)", 0.55,0.55,0.6) + GameTooltip:AddLine(" ") + CS.TipLine("偏斜减免(白字对BOSS):", 0.5,0.8,1) + CS.TipLine(" 300技能:约35%伤害损失", 0.55,0.55,0.6) + CS.TipLine(" 305技能:约15%伤害损失", 0.55,0.55,0.6) + CS.TipLine(" 315技能:无减免(全额)", 0.55,0.55,0.6) + end + end, + } + local rangedOnEnter = { + -- 远程攻强 + function() + local base, pos, neg = UnitRangedAttackPower("player") + local total = math.floor((base or 0) + (pos or 0) + (neg or 0)) + local bmod = math.floor((pos or 0) + (neg or 0)) + GameTooltip:AddLine("远程攻击强度 (RAP)", 1, 0.82, 0.0) + CS.TipLine("直接提升弓/枪/弩/投掷武器的伤害。") + CS.TipLine("每 14 点远程攻击强度 = +1 DPS。") + GameTooltip:AddLine(" ") + CS.TipLine("猎人核心属性:1 敏捷 = 2 远程AP。", 0.5,0.8,1) + GameTooltip:AddLine(" ") + CS.TipKV("基础 RAP:", tostring(math.floor(base or 0))) + if bmod ~= 0 then + CS.TipKV("装备/Buff 加成:", string.format("%+d", bmod), + 0.7,0.7,0.75, bmod > 0 and 0 or 1, bmod > 0 and 1 or 0, 0) + end + CS.TipKV("当前合计:", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6) + CS.TipKV("等效 DPS 增益:", string.format("+%.1f", total / 14), 0.7,0.7,0.75, 1,1,0.5) + end, + -- 远程伤害 + function() + local _, lo, hi = UnitRangedDamage("player") + lo = math.floor(lo or 0); hi = math.floor(hi or 0) + local rspd = UnitRangedDamage("player") or 2 + local avgDps = rspd > 0 and (lo + hi) / 2 / rspd or 0 + GameTooltip:AddLine("远程伤害", 1, 0.82, 0.0) + CS.TipLine("远程武器基础伤害 + 远程攻击强度加成后的实际伤害区间。") + GameTooltip:AddLine(" ") + CS.TipKV("伤害区间:", lo .. " - " .. hi) + CS.TipKV("远程 DPS:", string.format("%.1f", avgDps), 0.7,0.7,0.75, 1,1,0.5) + end, + -- 远程速度 + function() + local spd = UnitRangedDamage("player") or 0 + GameTooltip:AddLine("远程攻击速度", 1, 0.82, 0.0) + CS.TipLine("远程武器每次射击的间隔(秒)。") + CS.TipLine("受急速效果影响(如猎人急速射击天赋)。") + GameTooltip:AddLine(" ") + CS.TipKV("射击间隔:", string.format("%.2f 秒", spd)) + end, + -- 远程暴击 + function() + local fromAPI = CS.TryAPIs({ "GetRangedCritChance" }) > 0 + GameTooltip:AddLine("远程暴击率", 1, 0.82, 0.0) + CS.TipLine("远程暴击造成 200% 伤害。猎人:每 52.91 敏 = 1%。") + GameTooltip:AddLine(" ") + if fromAPI then + CS.TipKV("当前暴击率:", string.format("%.2f%%", CS.SafeGetRangedCrit()), 0.7,0.7,0.75, 1,1,0.5) + else + local total, base, agiC, gearC, talC = CS.FullRangedCrit() + CS.TipLine("来源分项:", 0.5,0.8,1) + if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end + if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) end + if gearC > 0 then CS.TipKV(" 装备暴击:", string.format("+%d%%", gearC)) end + if talC > 0 then + CS.TipKV(" 天赋暴击:", string.format("+%d%%", talC)) + for _, d in ipairs(CS.GetTalentDetailsFor("meleeCrit")) do + CS.TipLine(string.format(" %s (%d/%d) +%d%%", + d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6) + end + end + GameTooltip:AddLine(" ") + CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5) + CS.TipLine("(Buff 暴击未计入)", 0.8,0.5,0.3) + end + end, + } + + cy = catContentY + for i = 1, 5 do + local row + row, cy = Make2ColRow(cat2, meleeL[i], rangedL[i] ~= "" and rangedL[i] or nil, cy) + row.meleeKey = meleeK[i] + row.rangedKey = rangedK[i] + if row.hitL and meleeOnEnter[i] then + local fn = meleeOnEnter[i] + row.hitL:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:ClearLines() + fn() + GameTooltip:Show() + end) + row.hitL:SetScript("OnLeave", function() GameTooltip:Hide() end) + end + if row.hitR and rangedOnEnter[i] then + local fn = rangedOnEnter[i] + row.hitR:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_LEFT") + GameTooltip:ClearLines() + fn() + GameTooltip:Show() + end) + row.hitR:SetScript("OnLeave", function() GameTooltip:Hide() end) + end + table.insert(page.cat2Rows, row) + end + + -- Cat 3: Spell + Defense + local cat3 = CreateFrame("Frame", nil, statPanel) + cat3:SetAllPoints(statPanel) + cat3:Hide() + page.statCatFrames[3] = cat3 + page.cat3Rows = {} + local spellL = { "法伤", "治疗", "暴击", "命中", "" } + local spellK = { "spellDmg", "spellHeal", "spellCrit", "spellHit", nil } + local defL = { "护甲", "防御", "闪避", "招架", "格挡" } + local defK = { "armor", "defense", "dodge", "parry", "block" } + + local spellOnEnter = { + -- 法术伤害 + function() + local maxDmg = 0 + local perSchool = {} + local fromAPI = false + if GetSpellBonusDamage then + for s = 2, 7 do + local d = GetSpellBonusDamage(s) or 0 + perSchool[s] = d + if d > maxDmg then maxDmg = d; fromAPI = true end + end + end + if maxDmg == 0 then + local lib = CS.GetItemBonusLib and CS.GetItemBonusLib() or nil + if lib and lib.GetBonus then + local baseDmg = lib:GetBonus("DMG") or 0 + local schoolKeys = { + [2] = "FIREDMG", [3] = "NATUREDMG", [4] = "FROSTDMG", + [5] = "SHADOWDMG", [6] = "ARCANEDMG", [7] = "HOLYDMG", + } + for s = 2, 7 do + local sd = baseDmg + (lib:GetBonus(schoolKeys[s]) or 0) + perSchool[s] = sd + if sd > maxDmg then maxDmg = sd end + end + end + end + GameTooltip:AddLine("法术伤害加成", 1, 0.82, 0.0) + CS.TipLine("提升伤害法术输出,按系别独立计算。") + CS.TipLine("加成 = 法伤 × 系数(0.3~1.0)。") + GameTooltip:AddLine(" ") + CS.TipLine("乌龟服特色:", 0.5,0.8,1) + CS.TipLine(" 部分锁/皮甲装备带法伤属性", 0.5,0.8,1) + CS.TipLine(" 增强萨:震击获10%AP法伤", 0.5,0.8,1) + CS.TipLine(" 惩戒骑:十字军受33%SP加成", 0.5,0.8,1) + GameTooltip:AddLine(" ") + CS.TipKV("最高法术伤害:", tostring(math.floor(maxDmg)), 0.7,0.7,0.75, 1,0.9,0.6) + local snames = {[2]="火焰",[3]="自然",[4]="冰霜",[5]="暗影",[6]="奥术",[7]="神圣"} + for s = 2, 7 do + if perSchool[s] and perSchool[s] > 0 then + CS.TipKV(" " .. (snames[s] or s) .. ":", tostring(math.floor(perSchool[s]))) + end + end + if not fromAPI and maxDmg > 0 then + CS.TipLine("(来自装备扫描,Buff 加成未计入)", 0.8,0.5,0.3) + end + end, + -- 治疗 + function() + local heal = GetSpellBonusHealing and GetSpellBonusHealing() or 0 + local fromAPI = heal > 0 + if heal == 0 then + heal = CS.GetGearBonus("HEAL") + end + heal = math.floor(heal) + GameTooltip:AddLine("治疗加成", 1, 0.82, 0.0) + CS.TipLine("提升所有治疗法术的恢复量。") + CS.TipLine("实际加成 = 治疗加成 × 法术系数(通常 0.4~1.0)。") + GameTooltip:AddLine(" ") + CS.TipLine("神圣骑/恢复德/牧师核心属性。", 0.5,0.8,1) + CS.TipLine("骑士Ironclad:护甲1-2%→治疗量", 0.5,0.8,1) + GameTooltip:AddLine(" ") + CS.TipKV("当前治疗加成:", tostring(heal), 0.7,0.7,0.75, 1,0.9,0.6) + if not fromAPI and heal > 0 then + CS.TipLine("(来自装备扫描,Buff 加成未计入)", 0.8,0.5,0.3) + end + end, + -- 法术暴击 + function() + local fromAPI = CS.TryAPIsArg({ "GetSpellCritChance" }, 2) > 0 + GameTooltip:AddLine("法术暴击率", 1, 0.82, 0.0) + CS.TipLine("法术暴击造成 150% 伤害/治疗量(+50%)。") + GameTooltip:AddLine(" ") + if fromAPI then + CS.TipKV("当前法术暴击:", string.format("%.2f%%", CS.SafeGetSpellCrit()), 0.7,0.7,0.75, 1,1,0.5) + else + local total, base, intC, gearC, talC = CS.FullSpellCrit() + CS.TipLine("来源分项:", 0.5,0.8,1) + if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end + if intC > 0 then CS.TipKV(" 智力暴击:", string.format("%.2f%%", intC)) end + if gearC > 0 then CS.TipKV(" 装备暴击:", string.format("+%d%%", gearC)) end + if talC > 0 then + CS.TipKV(" 天赋暴击:", string.format("+%d%%", talC)) + for _, d in ipairs(CS.GetTalentDetailsFor("spellCrit")) do + CS.TipLine(string.format(" %s (%d/%d) +%d%%", + d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6) + end + end + GameTooltip:AddLine(" ") + CS.TipKV("合计法术暴击:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5) + CS.TipLine("(Buff 暴击未计入)", 0.8,0.5,0.3) + end + end, + -- 法术命中 + function() + local fromAPI = CS.TryAPIs({ "GetSpellHitModifier" }) > 0 + GameTooltip:AddLine("法术命中", 1, 0.82, 0.0) + CS.TipLine("减少法术被抵抗概率。法系最优先。") + CS.TipLine("对+3级BOSS基础未命中16%。") + GameTooltip:AddLine(" ") + if fromAPI then + CS.TipKV("当前法术命中:", string.format("%+d%%", CS.SafeGetSpellHit()), 0.7,0.7,0.75, 1,1,0.5) + else + local total, gearH, talH = CS.FullSpellHit() + CS.TipLine("来源分项:", 0.5,0.8,1) + if gearH > 0 then CS.TipKV(" 装备命中:", string.format("+%d%%", gearH)) end + if talH > 0 then + CS.TipKV(" 天赋命中:", string.format("+%d%%", talH)) + for _, d in ipairs(CS.GetTalentDetailsFor("spellHit")) do + CS.TipLine(string.format(" %s (%d/%d) +%d%%", + d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6) + end + CS.TipLine(" (天赋命中可能仅对特定系别)", 0.8,0.5,0.3) + end + GameTooltip:AddLine(" ") + CS.TipKV("合计法术命中:", string.format("+%d%%", total), 0.7,0.7,0.75, 1,1,0.5) + end + local totalHit = CS.SafeGetSpellHit() + local lvl = UnitLevel("player") or 60 + if lvl >= 60 then + GameTooltip:AddLine(" ") + local cap = 16 + CS.TipLine("达标(对+3级BOSS需16%):", 1, 0.82, 0.0) + if totalHit >= cap then + CS.TipKV(" 状态:", "已达标", 0.7,0.7,0.75, 0.3,1,0.3) + else + CS.TipKV(" 状态:", string.format("差%d%%", cap - totalHit), + 0.7,0.7,0.75, 1,0.3,0.3) + end + CS.TipLine(" 法师精准6%/奥术集中10%", 0.55,0.55,0.6) + CS.TipLine(" 术士镇压10%/暗牧集中10%", 0.55,0.55,0.6) + end + end, + } + local defOnEnter = { + -- 护甲 + function() + local baseArmor, effArmor = UnitArmor("player") + effArmor = math.floor(effArmor or baseArmor or 0) + baseArmor = math.floor(baseArmor or 0) + local bonusArmor = effArmor - baseArmor + local lvl = UnitLevel("player") or 60 + local pct = CS.CalcArmorReduction(effArmor, lvl) + local pctBoss = CS.CalcArmorReduction(effArmor, lvl + 3) + GameTooltip:AddLine("护甲", 1, 0.82, 0.0) + CS.TipLine("减少受到的物理伤害。减伤上限为 75%。") + GameTooltip:AddLine(" ") + CS.TipLine("公式:护甲/(护甲+400+85×等级)", 0.5,0.8,1) + CS.TipLine("来源:装备 + 敏捷护甲加成。", 0.5,0.8,1) + CS.TipLine("熊德/防战护甲需求最高。", 0.5,0.8,1) + GameTooltip:AddLine(" ") + CS.TipKV("基础护甲:", tostring(baseArmor)) + if bonusArmor ~= 0 then + CS.TipKV("Buff/敏捷加成:", string.format("%+d", bonusArmor), + 0.7,0.7,0.75, bonusArmor > 0 and 0 or 1, bonusArmor > 0 and 1 or 0, 0) + end + CS.TipKV("当前护甲:", tostring(effArmor), 0.7,0.7,0.75, 1,0.9,0.6) + GameTooltip:AddLine(" ") + CS.TipKV("减伤(同级):", string.format("%.1f%%", pct), 0.7,0.7,0.75, 0.4,1,0.4) + CS.TipKV("减伤(+3BOSS):", string.format("%.1f%%", pctBoss), 0.7,0.7,0.75, 1,0.7,0.3) + end, + -- 防御 + function() + local base, mod = UnitDefense("player") + base = math.floor(base or 0) + mod = math.floor(mod or 0) + local total = base + mod + local uncritCap = 440 + GameTooltip:AddLine("防御技能", 1, 0.82, 0.0) + CS.TipLine("提升闪避/招架/格挡,降低被暴击/碾压。") + GameTooltip:AddLine(" ") + CS.TipLine("每1点防御(超出攻击者):", 0.5,0.8,1) + CS.TipLine(" +0.04%闪避/招架/格挡") + CS.TipLine(" -0.04%被暴击/被碾压") + GameTooltip:AddLine(" ") + CS.TipLine("坦克达标(对63级BOSS):", 1, 0.82, 0.0) + CS.TipLine(" 免暴击:防御≥440(额外140)", 1,0.4,0.4) + CS.TipLine(" BOSS暴击率5.6%,每防-0.04%", 0.55,0.55,0.6) + GameTooltip:AddLine(" ") + CS.TipLine(" 免碾压(102.4%圆桌):", 1,0.4,0.4) + CS.TipLine(" 闪避+招架+格挡≥102.4%", 0.55,0.55,0.6) + CS.TipLine(" 碾压(150%伤)被排挤出圆桌", 0.55,0.55,0.6) + CS.TipLine(" 防战靠盾挡,防骑靠神圣之盾", 0.55,0.55,0.6) + GameTooltip:AddLine(" ") + CS.TipLine("坦怪仇恨压力大,命中不可忽视。", 0.5,0.8,1) + GameTooltip:AddLine(" ") + CS.TipKV("基础防御:", tostring(base)) + if mod ~= 0 then + CS.TipKV("装备加成:", string.format("%+d", mod), + 0.7,0.7,0.75, mod > 0 and 0 or 1, mod > 0 and 1 or 0, 0) + end + CS.TipKV("当前防御:", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6) + if total >= uncritCap then + CS.TipKV("免暴击(440):", "已达标", 0.7,0.7,0.75, 0.3,1,0.3) + else + CS.TipKV("免暴击(440):", string.format("差%d防御", uncritCap - total), + 0.7,0.7,0.75, 1,0.3,0.3) + end + end, + -- 闪避 + function() + local dodge = GetDodgeChance and GetDodgeChance() or 0 + if dodge == 0 then dodge = CS.GetGearBonus("DODGE") end + GameTooltip:AddLine("闪避", 1, 0.82, 0.0) + CS.TipLine("完全规避一次近战物理攻击。") + CS.TipLine("闪避时不受伤害,不触发命中特效。") + CS.TipLine("注意:对远程攻击无效。") + GameTooltip:AddLine(" ") + CS.TipLine("来源:敏捷+防御+天赋+装备。", 0.5,0.8,1) + CS.TipLine("熊德无法招架,闪避是主要减伤。", 0.5,0.8,1) + GameTooltip:AddLine(" ") + CS.TipKV("当前闪避率:", string.format("%.2f%%", dodge), 0.7,0.7,0.75, 1,1,0.5) + if (GetDodgeChance and GetDodgeChance() or 0) == 0 and dodge > 0 then + CS.TipLine("(仅含装备加成)", 0.8,0.5,0.3) + end + end, + -- 招架 + function() + local parry = GetParryChance and GetParryChance() or 0 + if parry == 0 then parry = CS.GetGearBonus("PARRY") end + GameTooltip:AddLine("招架", 1, 0.82, 0.0) + CS.TipLine("完全免疫一次近战攻击伤害,") + CS.TipLine("并使下次攻击加速40%(招架反击)。") + GameTooltip:AddLine(" ") + CS.TipLine("可招架:战士/骑士/盗贼/猎人。", 0.5,0.8,1) + CS.TipLine("熊德无法招架(仅靠闪避+护甲)。", 1,0.5,0.5) + CS.TipLine("只能招架正面攻击。", 0.5,0.8,1) + GameTooltip:AddLine(" ") + CS.TipKV("当前招架率:", string.format("%.2f%%", parry), 0.7,0.7,0.75, 1,1,0.5) + if (GetParryChance and GetParryChance() or 0) == 0 and parry > 0 then + CS.TipLine("(仅含装备加成)", 0.8,0.5,0.3) + end + end, + -- 格挡 + function() + local block = GetBlockChance and GetBlockChance() or 0 + local bv = 0 + if GetBlockValue then bv = GetBlockValue() or 0 end + GameTooltip:AddLine("格挡", 1, 0.82, 0.0) + CS.TipLine("盾牌格挡吸收等同格挡值的伤害。") + CS.TipLine("不能完全免伤,但大幅降低受伤。") + GameTooltip:AddLine(" ") + CS.TipLine("需装备盾牌。来源:防御+装备。", 0.5,0.8,1) + CS.TipLine("持盾:战士/圣骑/萨满。", 0.5,0.8,1) + CS.TipLine("BV = (力量-初始力量)/20", 0.5,0.8,1) + GameTooltip:AddLine(" ") + CS.TipLine("102.4%圆桌中格挡占重要份额。", 0.5,0.8,1) + GameTooltip:AddLine(" ") + CS.TipKV("当前格挡率:", string.format("%.2f%%", block), 0.7,0.7,0.75, 1,1,0.5) + if bv > 0 then + CS.TipKV("格挡值 (BV):", tostring(math.floor(bv)), 0.7,0.7,0.75, 0.4,1,0.4) + CS.TipLine("(每次格挡吸收该数值的物理伤害)", 0.55,0.55,0.6) + end + end, + } + + cy = catContentY + for i = 1, 5 do + local row + local lbl = spellL[i] ~= "" and spellL[i] or nil + row, cy = Make2ColRow(cat3, lbl, defL[i], cy) + row.spellKey = spellK[i] + row.defKey = defK[i] + if row.hitL and spellOnEnter[i] then + local fn = spellOnEnter[i] + row.hitL:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:ClearLines() + fn() + GameTooltip:Show() + end) + row.hitL:SetScript("OnLeave", function() GameTooltip:Hide() end) + end + if row.hitR and defOnEnter[i] then + local fn = defOnEnter[i] + row.hitR:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_LEFT") + GameTooltip:ClearLines() + fn() + GameTooltip:Show() + end) + row.hitR:SetScript("OnLeave", function() GameTooltip:Hide() end) + end + table.insert(page.cat3Rows, row) + end + + -- Set scroll content height (equipment area only, stats are fixed) + page.totalContentH = math.abs(infoY) + 4 + scrollArea:SetContentHeight(page.totalContentH) + + -- Initialize stat category visual (highlight first button) + CP:RefreshStatCatVisual(page) +end + +function CP:RefreshStatCatVisual(page) + for ci = 1, 3 do + if ci == page.activeStatCat then + page.statCatBtns[ci]:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) + page.statCatBtns[ci]:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) + page.statCatBtns[ci].label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + page.statCatFrames[ci]:Show() + else + page.statCatBtns[ci]:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) + page.statCatBtns[ci]:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], T.tabBorder[4]) + page.statCatBtns[ci].label:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + page.statCatFrames[ci]:Hide() + end + end +end + +-------------------------------------------------------------------------------- +-- Equipment Swap Popup (Alt+Hover to show bag alternatives) +-------------------------------------------------------------------------------- +local SLOT_TO_EQUIP_LOCS = { + [1] = { INVTYPE_HEAD = true }, + [2] = { INVTYPE_NECK = true }, + [3] = { INVTYPE_SHOULDER = true }, + [4] = { INVTYPE_BODY = true }, + [5] = { INVTYPE_CHEST = true, INVTYPE_ROBE = true }, + [6] = { INVTYPE_WAIST = true }, + [7] = { INVTYPE_LEGS = true }, + [8] = { INVTYPE_FEET = true }, + [9] = { INVTYPE_WRIST = true }, + [10] = { INVTYPE_HAND = true }, + [11] = { INVTYPE_FINGER = true }, + [12] = { INVTYPE_FINGER = true }, + [13] = { INVTYPE_TRINKET = true }, + [14] = { INVTYPE_TRINKET = true }, + [15] = { INVTYPE_CLOAK = true }, + [16] = { INVTYPE_WEAPON = true, INVTYPE_2HWEAPON = true, INVTYPE_WEAPONMAINHAND = true }, + [17] = { + INVTYPE_SHIELD = true, INVTYPE_WEAPONOFFHAND = true, INVTYPE_HOLDABLE = true, + INVTYPE_WEAPON = true, INVTYPE_WEAPONMAINHAND = true + }, + [18] = { INVTYPE_RANGED = true, INVTYPE_RANGEDRIGHT = true, INVTYPE_THROWN = true, INVTYPE_RELIC = true }, + [19] = { INVTYPE_TABARD = true }, +} + +local WEAPON_SLOT_EQUIP_LOCS = { + INVTYPE_WEAPON = true, + INVTYPE_2HWEAPON = true, + INVTYPE_WEAPONMAINHAND = true, + INVTYPE_WEAPONOFFHAND = true, + INVTYPE_SHIELD = true, + INVTYPE_HOLDABLE = true, +} + +local swapPopup +local SWAP_POPUP_W = 235 +local SWAP_ROW_H = 26 +local SWAP_MAX_VISIBLE = 8 +local SWAP_ICON_SIZE = 20 + +local function TryGetEquipLoc(link) + if not link then return nil end + local name, _, quality, _, _, _, _, equipLoc, tex = GetItemInfo(link) + if equipLoc and equipLoc ~= "" then + return name, quality, equipLoc, tex + end + local _, _, itemString = string.find(link, "(item:[%d:]+)") + if itemString then + name, _, quality, _, _, _, _, equipLoc, tex = GetItemInfo(itemString) + if equipLoc and equipLoc ~= "" then + return name, quality, equipLoc, tex + end + end + local _, _, itemId = string.find(link, "item:(%d+)") + if itemId then + name, _, quality, _, _, _, _, equipLoc, tex = GetItemInfo(tonumber(itemId)) + if equipLoc and equipLoc ~= "" then + return name, quality, equipLoc, tex + end + end + return nil +end + +local function ScanBagsForSlot(slotID) + local locSet = SLOT_TO_EQUIP_LOCS[slotID] + if not locSet then return {}, "no_mapping" end + local items = {} + local debugEquipLocs = {} + local isWeaponSlot = (slotID == 16 or slotID == 17) + for bag = 0, 4 do + local numSlots = GetContainerNumSlots(bag) + if numSlots and numSlots > 0 then + for slot = 1, numSlots do + local link = GetContainerItemLink(bag, slot) + if link then + local name, quality, equipLoc, tex = TryGetEquipLoc(link) + if equipLoc then + debugEquipLocs[equipLoc] = (debugEquipLocs[equipLoc] or 0) + 1 + local matched = locSet[equipLoc] + if (not matched) and isWeaponSlot and WEAPON_SLOT_EQUIP_LOCS[equipLoc] then + matched = true + end + if matched then + if not tex then tex = GetContainerItemInfo(bag, slot) end + if not quality then quality = GetItemQualityFromLink(link) end + local ilvl + if LibItem_Level then + local _, _, itemId = string.find(link, "item:(%d+)") + ilvl = itemId and LibItem_Level[tonumber(itemId)] + end + table.insert(items, { + bag = bag, slot = slot, link = link, + name = name or "?", texture = tex, + quality = quality or 1, + ilvl = ilvl, + }) + end + end + end + end + end + end + table.sort(items, function(a, b) + if a.quality ~= b.quality then return a.quality > b.quality end + if (a.ilvl or 0) ~= (b.ilvl or 0) then return (a.ilvl or 0) > (b.ilvl or 0) end + return a.name < b.name + end) + return items, debugEquipLocs +end + +local function GetOrCreateSwapPopup() + if swapPopup then return swapPopup end + + local f = CreateFrame("Frame", "SFramesCPSwapPopup", UIParent) + f:SetWidth(SWAP_POPUP_W) + f:SetHeight(60) + f:SetFrameStrata("TOOLTIP") + f:EnableMouse(true) + SetRoundBackdrop(f, T.bg, T.border) + CreateShadow(f, 3) + f:Hide() + + local title = MakeFS(f, 10, "CENTER", T.accentLight) + title:SetPoint("TOP", f, "TOP", 0, -6) + f.title = title + + local hint = f:CreateFontString(nil, "OVERLAY") + hint:SetFont(GetFont(), 8, "OUTLINE") + hint:SetTextColor(0.55, 0.55, 0.55) + hint:SetText("") + hint:SetPoint("BOTTOM", f, "BOTTOM", 0, 4) + f.hint = hint + + f.rows = {} + f.scrollOffset = 0 + f.totalItems = 0 + f.items = {} + f.anchorSlot = nil + + f:EnableMouseWheel(1) + f:SetScript("OnMouseWheel", function() + local maxOff = math.max(f.totalItems - SWAP_MAX_VISIBLE, 0) + if arg1 > 0 then + f.scrollOffset = math.max(f.scrollOffset - 1, 0) + else + f.scrollOffset = math.min(f.scrollOffset + 1, maxOff) + end + CP:RefreshSwapRows() + end) + + f:SetScript("OnUpdate", function() + if not IsAltKeyDown() then + f:Hide() + return + end + if panel and not panel:IsShown() then + f:Hide() + return + end + local overPopup = MouseIsOver and MouseIsOver(f) + local overSlot = f.anchorSlot and MouseIsOver and MouseIsOver(f.anchorSlot) + if not overPopup and not overSlot then + if not f._gracePeriod then + f._gracePeriod = GetTime() + elseif GetTime() - f._gracePeriod > 0.3 then + f._gracePeriod = nil + f:Hide() + end + else + f._gracePeriod = nil + end + end) + + f:SetScript("OnLeave", function() + end) + + swapPopup = f + return f +end + +function CP:RefreshSwapRows() + local popup = swapPopup + if not popup then return end + local items = popup.items + local offset = popup.scrollOffset + local total = table.getn(items) + local numVis = math.min(total - offset, SWAP_MAX_VISIBLE) + + for i = 1, table.getn(popup.rows) do + local row = popup.rows[i] + if i <= numVis then + local item = items[offset + i] + row.icon:SetTexture(item.texture) + row.nameText:SetText(item.name) + local qc = QUALITY_COLORS[item.quality] or QUALITY_COLORS[1] + row.nameText:SetTextColor(qc[1], qc[2], qc[3]) + row.ilvlText:SetText(item.ilvl and tostring(item.ilvl) or "") + row.itemBag = item.bag + row.itemSlot = item.slot + row.itemLink = item.link + row:Show() + else + row:Hide() + end + end +end + +function CP:ShowSwapPopup(slot) + local items, debugInfo = ScanBagsForSlot(slot.slotID) + local count = table.getn(items) + + local popup = GetOrCreateSwapPopup() + popup.anchorSlot = slot + popup.items = items + popup.totalItems = count + popup.scrollOffset = 0 + + local label = SLOT_LABEL[slot.slotName] or slot.slotName + + for i = 1, table.getn(popup.rows) do popup.rows[i]:Hide() end + + if count == 0 then + popup.title:SetText(label .. " - 背包中无可替换装备") + popup:SetHeight(30) + popup.hint:SetText("") + else + popup.title:SetText(label .. " - 可替换装备 (" .. count .. ")") + + local numVis = math.min(count, SWAP_MAX_VISIBLE) + local hasScroll = count > SWAP_MAX_VISIBLE + local hintH = hasScroll and 14 or 0 + popup:SetHeight(22 + numVis * SWAP_ROW_H + 6 + hintH) + popup.hint:SetText(hasScroll and "滚轮翻页" or "") + + for i = 1, numVis do + if not popup.rows[i] then + local row = CreateFrame("Button", nil, popup) + row:SetWidth(SWAP_POPUP_W - 12) + row:SetHeight(SWAP_ROW_H) + row:EnableMouse(true) + + local icon = row:CreateTexture(nil, "ARTWORK") + icon:SetPoint("LEFT", row, "LEFT", 2, 0) + icon:SetWidth(SWAP_ICON_SIZE) + icon:SetHeight(SWAP_ICON_SIZE) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + row.icon = icon + + local nameText = row:CreateFontString(nil, "OVERLAY") + nameText:SetFont(GetFont(), 10, "OUTLINE") + nameText:SetPoint("LEFT", icon, "RIGHT", 4, 0) + nameText:SetPoint("RIGHT", row, "RIGHT", -28, 0) + nameText:SetJustifyH("LEFT") + row.nameText = nameText + + local ilvlText = row:CreateFontString(nil, "OVERLAY") + ilvlText:SetFont(GetFont(), 9, "OUTLINE") + ilvlText:SetPoint("RIGHT", row, "RIGHT", -4, 0) + ilvlText:SetJustifyH("RIGHT") + ilvlText:SetTextColor(1, 0.82, 0) + row.ilvlText = ilvlText + + local hl = row:CreateTexture(nil, "HIGHLIGHT") + hl:SetTexture("Interface\\Buttons\\WHITE8X8") + hl:SetVertexColor(T.accentLight[1], T.accentLight[2], T.accentLight[3], 0.12) + hl:SetAllPoints(row) + + row:SetScript("OnClick", function() + UseContainerItem(this.itemBag, this.itemSlot) + popup:Hide() + CP:ScheduleEquipUpdate() + end) + + row:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetBagItem(this.itemBag, this.itemSlot) + GameTooltip:Show() + end) + + row:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + popup.rows[i] = row + end + popup.rows[i]:SetPoint("TOPLEFT", popup, "TOPLEFT", 6, -(20 + (i - 1) * SWAP_ROW_H)) + end + + for i = numVis + 1, table.getn(popup.rows) do + popup.rows[i]:Hide() + end + + self:RefreshSwapRows() + end + + popup:ClearAllPoints() + local slotLeft = slot:GetLeft() or 0 + local screenW = GetScreenWidth() or 1024 + if slotLeft + SLOT_SIZE + SWAP_POPUP_W + 10 > screenW then + popup:SetPoint("TOPRIGHT", slot, "TOPLEFT", -4, 0) + else + popup:SetPoint("TOPLEFT", slot, "TOPRIGHT", 4, 0) + end + popup:Show() +end + +function CP:HideSwapPopup() + if swapPopup then swapPopup:Hide() end +end + +function CP:CreateEquipSlot(parent, slotID, slotName) + local frame = CreateFrame("Button", NextName("Slot"), parent) + frame:SetWidth(SLOT_SIZE) + frame:SetHeight(SLOT_SIZE) + SetRoundBackdrop(frame, T.slotBg, T.slotBorder) + + local icon = frame:CreateTexture(nil, "ARTWORK") + icon:SetPoint("TOPLEFT", frame, "TOPLEFT", 4, -4) + icon:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -4, 4) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + frame.icon = icon + + local ilvlText = frame:CreateFontString(nil, "OVERLAY") + ilvlText:SetFont(GetFont(), 8, "OUTLINE") + ilvlText:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -2, 2) + ilvlText:SetTextColor(1, 0.82, 0) + ilvlText:SetText("") + frame.ilvlText = ilvlText + + local glow = frame:CreateTexture(nil, "OVERLAY") + glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + glow:SetBlendMode("ADD") + glow:SetAlpha(0.8) + glow:SetWidth(SLOT_SIZE * 2) + glow:SetHeight(SLOT_SIZE * 2) + glow:SetPoint("CENTER", frame, "CENTER", 0, 0) + glow:Hide() + frame.qualGlow = glow + + frame.slotID = slotID + frame.slotName = slotName + local _, emptyTex = GetInventorySlotInfo(slotName) + frame.emptyTexture = emptyTex + + frame:RegisterForClicks("LeftButtonUp", "RightButtonUp") + frame:RegisterForDrag("LeftButton") + + frame:SetScript("OnClick", function() + local sid = this.slotID + if CursorHasItem and CursorHasItem() then + PickupInventoryItem(sid) + elseif arg1 == "RightButton" then + if GetInventoryItemLink("player", sid) then + UseInventoryItem(sid) + end + elseif IsShiftKeyDown() and arg1 == "LeftButton" then + local link = GetInventoryItemLink("player", sid) + if link and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then + ChatFrameEditBox:Insert(link) + end + else + PickupInventoryItem(sid) + end + CP:ScheduleEquipUpdate() + end) + + frame:SetScript("OnDragStart", function() + PickupInventoryItem(this.slotID) + CP:ScheduleEquipUpdate() + end) + + frame:SetScript("OnReceiveDrag", function() + PickupInventoryItem(this.slotID) + CP:ScheduleEquipUpdate() + end) + + frame:SetScript("OnEnter", function() + this._mouseOver = true + this._swapActive = nil + this:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4]) + if IsAltKeyDown() then + this._swapActive = true + GameTooltip:Hide() + CP:ShowSwapPopup(this) + else + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + if GetInventoryItemLink("player", this.slotID) then + GameTooltip:SetInventoryItem("player", this.slotID) + else + local label = SLOT_LABEL[this.slotName] or this.slotName + GameTooltip:AddLine(label .. " - 未装备", 0.5, 0.5, 0.5) + end + GameTooltip:AddLine("[右键] 使用装备效果", 0.45, 0.45, 0.45) + GameTooltip:AddLine("[Alt] 查看可替换装备", 0.45, 0.45, 0.45) + GameTooltip:Show() + end + end) + + frame:SetScript("OnLeave", function() + this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + GameTooltip:Hide() + this._mouseOver = nil + this._swapActive = nil + end) + + frame:SetScript("OnUpdate", function() + if not this._mouseOver then return end + if IsAltKeyDown() then + if not this._swapActive then + this._swapActive = true + GameTooltip:Hide() + CP:ShowSwapPopup(this) + end + else + if this._swapActive then + this._swapActive = nil + if swapPopup then swapPopup:Hide() end + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + if GetInventoryItemLink("player", this.slotID) then + GameTooltip:SetInventoryItem("player", this.slotID) + else + local label = SLOT_LABEL[this.slotName] or this.slotName + GameTooltip:AddLine(label .. " - 未装备", 0.5, 0.5, 0.5) + end + GameTooltip:AddLine("[Alt] 查看可替换装备", 0.45, 0.45, 0.45) + GameTooltip:Show() + end + end + end) + + return frame +end + +local equipUpdateTimer = CreateFrame("Frame", nil, UIParent) +equipUpdateTimer:Hide() +equipUpdateTimer._elapsed = 0 +equipUpdateTimer._pending = 0 +equipUpdateTimer:SetScript("OnUpdate", function() + this._elapsed = this._elapsed + arg1 + if this._elapsed >= 0.15 then + this._elapsed = 0 + this._pending = this._pending - 1 + CP:UpdateEquipment() + if this._pending <= 0 then + this._pending = 0 + this:Hide() + end + end +end) + +function CP:ScheduleEquipUpdate() + equipUpdateTimer._elapsed = 0 + equipUpdateTimer._pending = 3 + equipUpdateTimer:Show() +end + +function CP:UpdateEquipment() + local page = pages[1] + if not page or not page.built then return end + + local showModel = not SFramesDB or SFramesDB.charShowModel ~= false + if page.model then + if showModel then + page.model:Show() + if page.modelFrame then page.modelFrame:Show() end + if page.modelBgFrame then page.modelBgFrame:Show() end + if page.modelToolbar then page.modelToolbar:Show() end + page.model:SetUnit("player") + page.model:SetFacing(page.model.curFacing or 0.4) + page.model:SetModelScale(page.model.curScale or 1.0) + page.model:SetPosition(page.model.posY or 0, 0, page.model.posX or 0) + else + page.model:Hide() + if page.modelFrame then page.modelFrame:Hide() end + if page.modelBgFrame then page.modelBgFrame:Hide() end + if page.modelToolbar then page.modelToolbar:Hide() end + end + end + local showGlow = not SFramesDB or SFramesDB.charShowQualityGlow ~= false + + local allSlots = {} + for _, t in ipairs({ EQUIP_SLOTS_LEFT, EQUIP_SLOTS_RIGHT, EQUIP_SLOTS_BOTTOM }) do + for _, s in ipairs(t) do table.insert(allSlots, s) end + end + + for _, si in ipairs(allSlots) do + local slot = page.equipSlots[si.id] + if slot then + local tex = GetInventoryItemTexture("player", si.id) + local link = GetInventoryItemLink("player", si.id) + if tex then + slot.icon:SetTexture(tex) + slot.icon:SetVertexColor(1, 1, 1) + if slot.ilvlText then + local showIlvl = not SFramesDB or SFramesDB.showItemLevel ~= false + if showIlvl and link and LibItem_Level then + local _, _, itemId = string.find(link, "item:(%d+)") + local ilvl = itemId and LibItem_Level[tonumber(itemId)] + slot.ilvlText:SetText(ilvl and tostring(ilvl) or "") + else + slot.ilvlText:SetText("") + end + end + local quality = GetItemQualityFromLink(link) + if quality and quality >= 2 and QUALITY_COLORS[quality] then + local qc = QUALITY_COLORS[quality] + if showGlow then + slot.qualGlow:SetVertexColor(qc[1], qc[2], qc[3]) + slot.qualGlow:Show() + else + slot.qualGlow:Hide() + end + else + slot.qualGlow:Hide() + end + else + if slot.emptyTexture then + slot.icon:SetTexture(slot.emptyTexture) + slot.icon:SetVertexColor(T.emptySlot[1], T.emptySlot[2], T.emptySlot[3], T.emptySlot[4]) + else slot.icon:SetTexture(nil) end + slot:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + slot.currentBorderColor = nil + slot.qualGlow:Hide() + if slot.ilvlText then slot.ilvlText:SetText("") end + end + end + end + + self:RefreshStatValues(page) +end + +local function GetStatVal(key) + if key == "meleeAP" then + local b, p, n = UnitAttackPower("player"); return tostring(math.floor((b or 0) + (p or 0) + (n or 0))) + elseif key == "meleeDmg" then + local lo, hi = UnitDamage("player"); return math.floor(lo or 0) .. "-" .. math.floor(hi or 0) + elseif key == "meleeSpeed" then + local ms, os = UnitAttackSpeed("player") + local t = string.format("%.1f", ms or 0) + if os and os > 0 then t = t .. "/" .. string.format("%.1f", os) end; return t + elseif key == "meleeCrit" then + return string.format("%.1f%%", SafeGetMeleeCrit()) + elseif key == "meleeHit" then + return string.format("%d%%", SafeGetMeleeHit()) + elseif key == "rangedAP" then + local b, p, n = UnitRangedAttackPower("player"); return tostring(math.floor((b or 0) + (p or 0) + (n or 0))) + elseif key == "rangedDmg" then + local _, lo, hi = UnitRangedDamage("player"); return math.floor(lo or 0) .. "-" .. math.floor(hi or 0) + elseif key == "rangedSpeed" then + local sp = UnitRangedDamage("player"); return string.format("%.1f", sp or 0) + elseif key == "rangedCrit" then + return string.format("%.1f%%", SafeGetRangedCrit()) + elseif key == "spellDmg" then + local mx = 0 + if GetSpellBonusDamage then for s = 2, 7 do local d = GetSpellBonusDamage(s); if d and d > mx then mx = d end end end + if mx == 0 then + local lib = GetItemBonusLib() + if lib then + local baseDmg = lib:GetBonus("DMG") or 0 + mx = baseDmg + local schools = { "FIREDMG","FROSTDMG","SHADOWDMG","ARCANEDMG","NATUREDMG","HOLYDMG" } + for _, sk in ipairs(schools) do + local sv = baseDmg + (lib:GetBonus(sk) or 0) + if sv > mx then mx = sv end + end + end + end + return tostring(math.floor(mx)) + elseif key == "spellHeal" then + local v = GetSpellBonusHealing and GetSpellBonusHealing() or 0 + if v == 0 then v = GetGearBonus("HEAL") end + return tostring(math.floor(v)) + elseif key == "spellCrit" then + return string.format("%.1f%%", SafeGetSpellCrit()) + elseif key == "spellHit" then + return string.format("%d%%", SafeGetSpellHit()) + elseif key == "armor" then + local b, e = UnitArmor("player"); return tostring(math.floor(e or b or 0)) + elseif key == "defense" then + local b, m = UnitDefense("player"); return tostring(math.floor((b or 0) + (m or 0))) + elseif key == "dodge" then + local v = GetDodgeChance and GetDodgeChance() or 0 + if v == 0 then v = GetGearBonus("DODGE") end + return string.format("%.1f%%", v) + elseif key == "parry" then + local v = GetParryChance and GetParryChance() or 0 + if v == 0 then v = GetGearBonus("PARRY") end + return string.format("%.1f%%", v) + elseif key == "block" then + local v = GetBlockChance and GetBlockChance() or 0 + if v == 0 then v = GetGearBonus("BLOCK") end + return string.format("%.1f%%", v) + end + return "0" +end + +function CP:RefreshStatValues(page) + if not page.built then return end + CP:RefreshStatCatVisual(page) + + -- Cat 1: base (left) + resist (right) + if page.cat1Rows then + for _, row in ipairs(page.cat1Rows) do + local stat, eff, pos, neg = UnitStat("player", row.baseIdx) + local e = eff or stat or 0 + row.valueL:SetText(tostring(math.floor(e))) + if row.valueR and row.resistSchool then + local b, total = UnitResistance("player", row.resistSchool) + row.valueR:SetText(tostring(math.floor(total or b or 0))) + end + end + end + + -- Cat 2: melee (left) + ranged (right) + if page.cat2Rows then + for _, row in ipairs(page.cat2Rows) do + if row.meleeKey then row.valueL:SetText(GetStatVal(row.meleeKey)) end + if row.rangedKey and row.valueR then row.valueR:SetText(GetStatVal(row.rangedKey)) end + end + end + + -- Cat 3: spell (left) + defense (right) + if page.cat3Rows then + for _, row in ipairs(page.cat3Rows) do + if row.spellKey and row.valueL then row.valueL:SetText(GetStatVal(row.spellKey)) end + if row.defKey and row.valueR then row.valueR:SetText(GetStatVal(row.defKey)) end + end + end +end + +-------------------------------------------------------------------------------- +-- Tab 2: Reputation (was Tab 3) +-------------------------------------------------------------------------------- +function CP:BuildReputationPage() + local page = pages[2] + if page.built then return end + page.built = true + local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4 + local scrollArea = CreateScrollFrame(page, CONTENT_W, contentH) + scrollArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + page.scrollArea = scrollArea + page.repRows = {} +end + +function CP:UpdateReputation() + local page = pages[2] + if not page or not page.built then return end + local child = page.scrollArea.child + if page.repRows then + for _, row in ipairs(page.repRows) do if row.frame then row.frame:Hide() end end + end + page.repRows = {} + + local numFactions = GetNumFactions() + local y = -6 + local rowH, barH, headerH = 30, 9, 24 + + for i = 1, numFactions do + local name, desc, standingId, barMin, barMax, barValue, atWar, canWar, isHeader, isCollapsed, hasRep, isWatched, isChild + if GetFactionInfo then + name, desc, standingId, barMin, barMax, barValue, atWar, canWar, isHeader, isCollapsed, hasRep, isWatched, isChild = GetFactionInfo(i) + end + if not name then break end + + if isHeader then + local hf = CreateFrame("Button", nil, child) + hf:SetWidth(SCROLL_W - 16) + hf:SetHeight(headerH) + hf:SetPoint("TOPLEFT", child, "TOPLEFT", 8, y) + local arrow = MakeFS(hf, 10, "LEFT", T.dimText) + arrow:SetPoint("LEFT", hf, "LEFT", 0, 0) + arrow:SetText(isCollapsed and "+" or "-") + local ht = MakeFS(hf, 11, "LEFT", T.sectionTitle) + ht:SetPoint("LEFT", arrow, "RIGHT", 3, 0) + ht:SetText(name) + hf.factionIndex = i + hf:SetScript("OnClick", function() + if isCollapsed then ExpandFactionHeader(this.factionIndex) else CollapseFactionHeader(this.factionIndex) end + CP:UpdateReputation() + end) + table.insert(page.repRows, { frame = hf }) + y = y - headerH + else + local rf = CreateFrame("Frame", nil, child) + rf:SetWidth(SCROLL_W - 24) + rf:SetHeight(rowH) + rf:SetPoint("TOPLEFT", child, "TOPLEFT", isChild and 22 or 14, y) + local nfs = MakeFS(rf, 9, "LEFT", T.valueText) + nfs:SetPoint("TOPLEFT", rf, "TOPLEFT", 0, -2) + nfs:SetText(name or "") + local rc = T.repColors[standingId] or { 0.5, 0.5, 0.5 } + local sfs = MakeFS(rf, 8, "RIGHT", rc) + sfs:SetPoint("TOPRIGHT", rf, "TOPRIGHT", 0, -2) + sfs:SetText(REP_STANDING[standingId] or "") + + local bf = CreateFrame("Frame", nil, rf) + bf:SetHeight(barH) + bf:SetPoint("BOTTOMLEFT", rf, "BOTTOMLEFT", 0, 2) + bf:SetPoint("BOTTOMRIGHT", rf, "BOTTOMRIGHT", 0, 2) + SetPixelBackdrop(bf, T.barBg, { 0.15, 0.15, 0.18, 0.5 }) + local bar = bf:CreateTexture(nil, "ARTWORK") + bar:SetTexture(SFrames:GetTexture()) + bar:SetVertexColor(rc[1], rc[2], rc[3], 0.85) + bar:SetPoint("TOPLEFT", bf, "TOPLEFT", 1, -1) + bar:SetPoint("BOTTOMLEFT", bf, "BOTTOMLEFT", 1, 1) + local range = math.max((barMax or 1) - (barMin or 0), 1) + local fill = Clamp(((barValue or 0) - (barMin or 0)) / range, 0, 1) + bar:SetWidth(math.max((SCROLL_W - 26) * fill, 1)) + local vfs = MakeFS(bf, 7, "CENTER", { 0.85, 0.85, 0.85 }) + vfs:SetPoint("CENTER", bf, "CENTER", 0, 0) + vfs:SetText(math.floor((barValue or 0) - (barMin or 0)) .. "/" .. math.floor(range)) + table.insert(page.repRows, { frame = rf }) + y = y - rowH + end + end + page.scrollArea:SetContentHeight(math.abs(y) + 16) +end + +-------------------------------------------------------------------------------- +-- Tab 3: Skills (was Tab 4) +-------------------------------------------------------------------------------- +function CP:BuildSkillsPage() + local page = pages[3] + if page.built then return end + page.built = true + local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4 + local scrollArea = CreateScrollFrame(page, CONTENT_W, contentH) + scrollArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + page.scrollArea = scrollArea + page.skillRows = {} +end + +function CP:UpdateSkills() + local page = pages[3] + if not page or not page.built then return end + local child = page.scrollArea.child + if page.skillRows then + for _, row in ipairs(page.skillRows) do if row.frame then row.frame:Hide() end end + end + page.skillRows = {} + + local numSkills = GetNumSkillLines and GetNumSkillLines() or 0 + local y = -6 + local rowH, barH, headerH = 28, 7, 24 + + for i = 1, numSkills do + local sn, isH, isE, sr, nt, sm, smr + if GetSkillLineInfo then sn, isH, isE, sr, nt, sm, smr = GetSkillLineInfo(i) end + if not sn then break end + + if isH then + local hf = CreateFrame("Button", nil, child) + hf:SetWidth(SCROLL_W - 16) + hf:SetHeight(headerH) + hf:SetPoint("TOPLEFT", child, "TOPLEFT", 8, y) + local arrow = MakeFS(hf, 10, "LEFT", T.dimText) + arrow:SetPoint("LEFT", hf, "LEFT", 0, 0) + arrow:SetText(isE and "-" or "+") + local ht = MakeFS(hf, 11, "LEFT", T.sectionTitle) + ht:SetPoint("LEFT", arrow, "RIGHT", 3, 0) + ht:SetText(sn) + hf.skillIndex = i + hf:SetScript("OnClick", function() + if isE then CollapseSkillHeader(this.skillIndex) else ExpandSkillHeader(this.skillIndex) end + CP:UpdateSkills() + end) + table.insert(page.skillRows, { frame = hf }) + y = y - headerH + else + local sf = CreateFrame("Frame", nil, child) + sf:SetWidth(SCROLL_W - 24) + sf:SetHeight(rowH) + sf:SetPoint("TOPLEFT", child, "TOPLEFT", 18, y) + local nfs = MakeFS(sf, 9, "LEFT", T.valueText) + nfs:SetPoint("TOPLEFT", sf, "TOPLEFT", 0, -2) + nfs:SetText(sn or "") + local rt = tostring(sr or 0) + if smr and smr > 0 then rt = rt .. "/" .. tostring(smr) end + local rfs = MakeFS(sf, 8, "RIGHT", T.dimText) + rfs:SetPoint("TOPRIGHT", sf, "TOPRIGHT", 0, -2) + rfs:SetText(rt) + if smr and smr > 0 then + local bf = CreateFrame("Frame", nil, sf) + bf:SetHeight(barH) + bf:SetPoint("BOTTOMLEFT", sf, "BOTTOMLEFT", 0, 2) + bf:SetPoint("BOTTOMRIGHT", sf, "BOTTOMRIGHT", 0, 2) + SetPixelBackdrop(bf, T.barBg, { 0.15, 0.15, 0.18, 0.5 }) + local bar = bf:CreateTexture(nil, "ARTWORK") + bar:SetTexture(SFrames:GetTexture()) + bar:SetVertexColor(0.4, 0.65, 0.85, 0.85) + bar:SetPoint("TOPLEFT", bf, "TOPLEFT", 1, -1) + bar:SetPoint("BOTTOMLEFT", bf, "BOTTOMLEFT", 1, 1) + bar:SetWidth(math.max((SCROLL_W - 26) * Clamp((sr or 0) / smr, 0, 1), 1)) + end + table.insert(page.skillRows, { frame = sf }) + y = y - rowH + end + end + page.scrollArea:SetContentHeight(math.abs(y) + 16) +end + +-------------------------------------------------------------------------------- +-- Tab 4: Honor (was Tab 5) +-------------------------------------------------------------------------------- +function CP:BuildHonorPage() + local page = pages[4] + if page.built then return end + page.built = true + + local pad = SIDE_PAD + 2 + page.rankIcon = page:CreateTexture(nil, "ARTWORK") + page.rankIcon:SetWidth(42) + page.rankIcon:SetHeight(42) + page.rankIcon:SetPoint("TOPLEFT", page, "TOPLEFT", pad, -16) + + page.rankName = MakeFS(page, 13, "LEFT", T.gold) + page.rankName:SetPoint("LEFT", page.rankIcon, "RIGHT", 10, 5) + page.rankProgress = MakeFS(page, 10, "LEFT", T.dimText) + page.rankProgress:SetPoint("LEFT", page.rankIcon, "RIGHT", 10, -8) + + local barY = -68 + local bf = CreateFrame("Frame", nil, page) + bf:SetHeight(12) + bf:SetPoint("TOPLEFT", page, "TOPLEFT", pad, barY) + bf:SetPoint("TOPRIGHT", page, "TOPRIGHT", -pad, barY) + SetPixelBackdrop(bf, T.barBg, { 0.15, 0.15, 0.18, 0.5 }) + page.rankBarFrame = bf + + local bfill = bf:CreateTexture(nil, "ARTWORK") + bfill:SetTexture(SFrames:GetTexture()) + bfill:SetVertexColor(0.85, 0.6, 0.1, 0.9) + bfill:SetPoint("TOPLEFT", bf, "TOPLEFT", 1, -1) + bfill:SetPoint("BOTTOMLEFT", bf, "BOTTOMLEFT", 1, 1) + bfill:SetWidth(1) + page.rankBarFill = bfill + + page.rankBarText = MakeFS(bf, 8, "CENTER", { 1, 1, 1 }) + page.rankBarText:SetPoint("CENTER", bf, "CENTER", 0, 0) + + local sY = barY - 26 + sY = self:CreateStatSection(page, "荣誉统计", sY) + page.honorStats = {} + local hl = { "本次击杀", "本次荣誉", "昨日击杀", "昨日荣誉", "本周击杀", "上周击杀", "上周荣誉", "上周排名", "终身击杀" } + for _, lbl in ipairs(hl) do + local row; row, sY = self:CreateStatRow(page, lbl, sY) + table.insert(page.honorStats, { row = row }) + end +end + +function CP:UpdateHonor() + local page = pages[4] + if not page or not page.built then return end + + local rn, rnum + if GetPVPRankInfo then rn, rnum = GetPVPRankInfo(UnitPVPRank("player")) end + rn = rn or "无军衔"; rnum = rnum or 0 + page.rankName:SetText(rn) + if rnum > 0 then + page.rankIcon:SetTexture(string.format("Interface\\PvPRankBadges\\PvPRank%02d", rnum)) + else page.rankIcon:SetTexture(nil) end + + local prog = GetPVPRankProgress and GetPVPRankProgress() or 0 + page.rankProgress:SetText(string.format("进度: %.1f%%", prog * 100)) + local bw = page.rankBarFrame:GetWidth() - 2 + page.rankBarFill:SetWidth(math.max(bw * prog, 1)) + page.rankBarText:SetText(string.format("%.1f%%", prog * 100)) + + local thk, tho = 0, 0; if GetPVPSessionStats then thk, tho = GetPVPSessionStats() end + local yhk, yho = 0, 0; if GetPVPYesterdayStats then yhk, yho = GetPVPYesterdayStats() end + local whk = 0; if GetPVPThisWeekStats then whk = GetPVPThisWeekStats() end + local lhk, lho, ls = 0, 0, 0; if GetPVPLastWeekStats then lhk, lho, ls = GetPVPLastWeekStats() end + local ltk = 0; if GetPVPLifetimeStats then ltk = GetPVPLifetimeStats() end + + local vals = { + tostring(thk or 0), tostring(math.floor(tho or 0)), + tostring(yhk or 0), tostring(math.floor(yho or 0)), + tostring(whk or 0), tostring(lhk or 0), + tostring(math.floor(lho or 0)), tostring(ls or 0), tostring(ltk or 0), + } + for i, stat in ipairs(page.honorStats) do + stat.row.value:SetText(vals[i] or "0") + end +end + +-------------------------------------------------------------------------------- +-- Events +-------------------------------------------------------------------------------- +local eventFrame = CreateFrame("Frame", "SFramesCPEvents", UIParent) +local cpEvents = { + "UNIT_INVENTORY_CHANGED", "PLAYER_AURAS_CHANGED", + "UPDATE_FACTION", "SKILL_LINES_CHANGED", + "PLAYER_PVP_KILLS_CHANGED", "PLAYER_PVP_RANK_CHANGED", + "UNIT_ATTACK_POWER", "UNIT_RANGEDDAMAGE", + "UNIT_ATTACK", "UNIT_DEFENSE", "UNIT_RESISTANCES", + "CHAT_MSG_SKILL", "CHAT_MSG_COMBAT_HONOR_GAIN", + "CHARACTER_POINTS_CHANGED", "PLAYER_ENTERING_WORLD", +} +for _, ev in ipairs(cpEvents) do + pcall(function() eventFrame:RegisterEvent(ev) end) +end +eventFrame:SetScript("OnEvent", function() + if not panel or not panel:IsShown() then return end + if event == "UNIT_INVENTORY_CHANGED" then + CP:ScheduleEquipUpdate() + end + CP:UpdateCurrentTab() +end) + +-------------------------------------------------------------------------------- +-- Hook: replace ToggleCharacter +-------------------------------------------------------------------------------- +-- Save CharacterFrame's original Show before HideBlizzardFrames may suppress it +local origCharFrameShow = CharacterFrame and CharacterFrame.Show +local origToggleCharacter = ToggleCharacter +ToggleCharacter = function(tab) + if SFramesDB and SFramesDB.charPanelEnable == false then + if CharacterFrame then + if origCharFrameShow then + CharacterFrame.Show = origCharFrameShow + end + if origToggleCharacter then + origToggleCharacter(tab) + else + if CharacterFrame:IsShown() then + HideUIPanel(CharacterFrame) + else + ShowUIPanel(CharacterFrame) + end + end + end + return + end + local tabMap = { + ["PaperDollFrame"] = 1, + ["ReputationFrame"] = 2, + ["SkillFrame"] = 3, + ["HonorFrame"] = 4, + ["PetPaperDollFrame"] = 1, + } + CP:Toggle(tabMap[tab] or 1) +end diff --git a/Chat.lua b/Chat.lua new file mode 100644 index 0000000..5ceb128 --- /dev/null +++ b/Chat.lua @@ -0,0 +1,6897 @@ +-------------------------------------------------------------------------------- +-- S-Frames: Chat takeover skin (Chat.lua) +-- Keeps Blizzard chat backend while replacing visuals and tab/filter workflow. +-------------------------------------------------------------------------------- + +SFrames.Chat = SFrames.Chat or {} + +local DEFAULTS = { + enable = true, + showBorder = false, + borderClassColor = false, + showPlayerLevel = true, + width = 360, + height = 220, + scale = 1, + fontSize = 12, + sidePadding = 10, + topPadding = 30, + bottomPadding = 8, + bgAlpha = 0.45, + activeTab = 1, + editBoxPosition = "bottom", + editBoxX = 0, + editBoxY = 200, +} + +local DEFAULT_COMBAT_TAB_NAME = COMBATLOG or COMBAT_LOG or "Combat" + +local HIDDEN_OBJECTS = { + "ChatFrameMenuButton", + "ChatFrameChannelButton", + "ChatFrameToggleVoiceMuteButton", + "ChatFrameToggleVoiceDeafenButton", + "ChatFrameToggleVoiceSelfMuteButton", + "ChatFrameToggleVoiceSelfDeafenButton", + "ChatFrameUpButton", + "ChatFrameDownButton", + "ChatFrameBottomButton", +} + +local EDITBOX_TEXTURES = { + "ChatFrameEditBoxLeft", + "ChatFrameEditBoxMid", + "ChatFrameEditBoxRight", + "ChatFrameEditBoxFocusLeft", + "ChatFrameEditBoxFocusMid", + "ChatFrameEditBoxFocusRight", + "ChatFrame1EditBoxLeft", + "ChatFrame1EditBoxMid", + "ChatFrame1EditBoxRight", + "ChatFrame1EditBoxFocusLeft", + "ChatFrame1EditBoxFocusMid", + "ChatFrame1EditBoxFocusRight", +} + +local FILTER_DEFS = { + { key = "say", label = "说话" }, + { key = "yell", label = "大喊" }, + { key = "emote", label = "动作" }, + { key = "guild", label = "公会" }, + { key = "party", label = "小队" }, + { key = "raid", label = "团队" }, + { key = "whisper", label = "密语" }, + { key = "system", label = "系统" }, + { key = "loot", label = "拾取" }, + { key = "money", label = "获益" }, +} +local DEFAULT_FILTERS = { + say = true, + yell = true, + emote = true, + guild = true, + party = true, + raid = true, + whisper = true, + system = true, + loot = false, + money = false, +} + +local AUTO_TRANSLATE_TARGET_LANG = "zh" + +local TRANSLATE_FILTER_KEYS = { + say = true, + yell = true, + emote = true, + guild = true, + party = true, + raid = true, + whisper = true, +} + +local TRANSLATE_FILTER_ORDER = { + "say", + "yell", + "emote", + "guild", + "party", + "raid", + "whisper", +} + +local FILTER_GROUPS = { + say = { "SAY" }, + yell = { "YELL" }, + emote = { "EMOTE", "TEXT_EMOTE", "MONSTER_EMOTE" }, + guild = { "GUILD", "OFFICER" }, + party = { "PARTY", "PARTY_LEADER" }, + raid = { "RAID", "RAID_LEADER", "RAID_WARNING", "BATTLEGROUND", "BATTLEGROUND_LEADER" }, + whisper = { "WHISPER", "WHISPER_INFORM", "REPLY", "AFK", "DND", "IGNORED", "MONSTER_WHISPER", "MONSTER_BOSS_WHISPER" }, + channel = { "CHANNEL", "CHANNEL_JOIN", "CHANNEL_LEAVE", "CHANNEL_LIST", "CHANNEL_NOTICE", "BG_HORDE", "BG_ALLIANCE" }, + system = { "SYSTEM", "MONSTER_SAY", "MONSTER_YELL", "MONSTER_BOSS_EMOTE" }, + loot = { "LOOT", "OPENING", "TRADESKILLS" }, + money = { "MONEY", "COMBAT_XP_GAIN", "COMBAT_HONOR_GAIN", "COMBAT_FACTION_CHANGE", "SKILL", "PET_INFO", "COMBAT_MISC_INFO" }, +} + +local ALL_MESSAGE_GROUPS = { + "SYSTEM", + "SAY", + "YELL", + "EMOTE", + "TEXT_EMOTE", + "MONSTER_SAY", + "MONSTER_YELL", + "MONSTER_EMOTE", + "MONSTER_WHISPER", + "MONSTER_BOSS_EMOTE", + "MONSTER_BOSS_WHISPER", + "WHISPER", + "WHISPER_INFORM", + "REPLY", + "AFK", + "DND", + "IGNORED", + "GUILD", + "OFFICER", + "PARTY", + "PARTY_LEADER", + "RAID", + "RAID_LEADER", + "RAID_WARNING", + "BATTLEGROUND", + "BATTLEGROUND_LEADER", + "CHANNEL", + "CHANNEL_JOIN", + "CHANNEL_LEAVE", + "CHANNEL_LIST", + "CHANNEL_NOTICE", + "BG_HORDE", + "BG_ALLIANCE", + "LOOT", + "OPENING", + "TRADESKILLS", + "MONEY", + "COMBAT_XP_GAIN", + "COMBAT_HONOR_GAIN", + "COMBAT_FACTION_CHANGE", + "SKILL", + "PET_INFO", + "COMBAT_MISC_INFO", +} + +local TRANSLATE_EVENT_FILTERS = { + CHAT_MSG_SAY = "say", + CHAT_MSG_YELL = "yell", + CHAT_MSG_EMOTE = "emote", + CHAT_MSG_TEXT_EMOTE = "emote", + CHAT_MSG_GUILD = "guild", + CHAT_MSG_OFFICER = "guild", + CHAT_MSG_PARTY = "party", + CHAT_MSG_PARTY_LEADER = "party", + CHAT_MSG_RAID = "raid", + CHAT_MSG_RAID_LEADER = "raid", + CHAT_MSG_RAID_WARNING = "raid", + CHAT_MSG_BATTLEGROUND = "raid", + CHAT_MSG_BATTLEGROUND_LEADER = "raid", + CHAT_MSG_WHISPER = "whisper", + CHAT_MSG_REPLY = "whisper", + CHAT_MSG_MONSTER_WHISPER = "whisper", + CHAT_MSG_MONSTER_BOSS_WHISPER = "whisper", + CHAT_MSG_CHANNEL = "channel", +} + +local function Clamp(value, minValue, maxValue) + value = tonumber(value) or minValue + if value < minValue then return minValue end + if value > maxValue then return maxValue end + return value +end + +-- ============================================================ +-- 共享:玩家职业颜色工具(供 Chat / Roll 等模块复用) +-- ============================================================ +SFrames.ClassColorHex = SFrames.ClassColorHex or { + WARRIOR = "c79c6e", + PALADIN = "f58cba", + HUNTER = "abd473", + ROGUE = "fff569", + PRIEST = "ffffff", + SHAMAN = "0070de", + MAGE = "69ccf0", + WARLOCK = "9482c9", + DRUID = "ff7d0a", +} + +SFrames.PlayerClassColorCache = SFrames.PlayerClassColorCache or {} +SFrames.PlayerLevelCache = SFrames.PlayerLevelCache or {} + +local function EnsureGlobalClassCache() + if not SFramesGlobalDB then SFramesGlobalDB = {} end + if not SFramesGlobalDB.classColorCache then SFramesGlobalDB.classColorCache = {} end + return SFramesGlobalDB.classColorCache +end + +local function EnsureGlobalLevelCache() + if not SFramesGlobalDB then SFramesGlobalDB = {} end + if not SFramesGlobalDB.levelCache then SFramesGlobalDB.levelCache = {} end + return SFramesGlobalDB.levelCache +end + +local function LoadPersistentClassCache() + local persistent = EnsureGlobalClassCache() + local runtime = SFrames.PlayerClassColorCache + for name, hex in pairs(persistent) do + if not runtime[name] then + runtime[name] = hex + end + end + local persistentLvl = EnsureGlobalLevelCache() + local runtimeLvl = SFrames.PlayerLevelCache + for name, lvl in pairs(persistentLvl) do + if not runtimeLvl[name] then + runtimeLvl[name] = lvl + end + end +end + +local function PersistClassCache() + local persistent = EnsureGlobalClassCache() + for name, hex in pairs(SFrames.PlayerClassColorCache) do + persistent[name] = hex + end + local persistentLvl = EnsureGlobalLevelCache() + for name, lvl in pairs(SFrames.PlayerLevelCache) do + persistentLvl[name] = lvl + end +end + +do + local reverseClassMap = nil + function SFrames:LocalizedClassToEN(localizedName) + if not localizedName then return nil end + if not reverseClassMap then + reverseClassMap = {} + local classTable = LOCALIZED_CLASS_NAMES_MALE or {} + for en, loc in pairs(classTable) do + reverseClassMap[loc] = en + reverseClassMap[string.upper(loc)] = en + end + local classTableF = LOCALIZED_CLASS_NAMES_FEMALE or {} + for en, loc in pairs(classTableF) do + reverseClassMap[loc] = en + reverseClassMap[string.upper(loc)] = en + end + local fallback = { + ["战士"] = "WARRIOR", ["圣骑士"] = "PALADIN", ["猎人"] = "HUNTER", + ["盗贼"] = "ROGUE", ["牧师"] = "PRIEST", ["萨满祭司"] = "SHAMAN", + ["法师"] = "MAGE", ["术士"] = "WARLOCK", ["德鲁伊"] = "DRUID", + } + for loc, en in pairs(fallback) do + if not reverseClassMap[loc] then reverseClassMap[loc] = en end + end + end + return reverseClassMap[localizedName] or reverseClassMap[string.upper(localizedName)] + end +end + +function SFrames:RefreshClassColorCache() + local now = GetTime() + if self._lastClassCacheRefresh and (now - self._lastClassCacheRefresh) < 2 then + return + end + self._lastClassCacheRefresh = now + + local cache = self.PlayerClassColorCache + local lvlCache = self.PlayerLevelCache + local hex = self.ClassColorHex + if UnitName and UnitClass then + local selfName = UnitName("player") + local _, selfClass = UnitClass("player") + if selfName and selfClass and hex[selfClass] then + cache[selfName] = hex[selfClass] + end + if selfName and UnitLevel then + local selfLevel = UnitLevel("player") + if selfLevel and selfLevel > 0 then + lvlCache[selfName] = selfLevel + end + end + end + if GetNumRaidMembers then + local count = GetNumRaidMembers() + for i = 1, count do + local rName, _, _, rLevel, _, rClass = GetRaidRosterInfo(i) + if rName and rClass and hex[rClass] then + cache[rName] = hex[rClass] + end + if rName and rLevel and rLevel > 0 then + lvlCache[rName] = rLevel + end + end + end + if GetNumPartyMembers and UnitName and UnitClass then + local count = GetNumPartyMembers() + for i = 1, count do + local unit = "party" .. i + local pName = UnitName(unit) + local _, pClass = UnitClass(unit) + if pName and pClass and hex[pClass] then + cache[pName] = hex[pClass] + end + if pName and UnitLevel then + local pLevel = UnitLevel(unit) + if pLevel and pLevel > 0 then + lvlCache[pName] = pLevel + end + end + end + end + if GetNumGuildMembers then + local count = GetNumGuildMembers() + for i = 1, count do + local gName, _, _, gLevel, gClassLoc, _, _, _, _, _, classEN = GetGuildRosterInfo(i) + if gName then + if not classEN or classEN == "" then + classEN = self:LocalizedClassToEN(gClassLoc) + end + if classEN and hex[string.upper(classEN)] then + cache[gName] = hex[string.upper(classEN)] + end + if gLevel and gLevel > 0 then + lvlCache[gName] = gLevel + end + end + end + end + if GetNumFriends then + local count = GetNumFriends() + for i = 1, count do + local fName, fLevel, fClass, fArea, fConnected = GetFriendInfo(i) + if fName and fClass then + local classEN = self:LocalizedClassToEN(fClass) + if classEN and hex[classEN] then + cache[fName] = hex[classEN] + end + end + if fName and fLevel and fLevel > 0 then + lvlCache[fName] = fLevel + end + end + end + if GetNumWhoResults then + local count = GetNumWhoResults() + for i = 1, count do + local wName, wGuild, wLevel, wRace, wClassEN = GetWhoInfo(i) + if wName and wClassEN and wClassEN ~= "" and hex[string.upper(wClassEN)] then + cache[wName] = hex[string.upper(wClassEN)] + end + if wName and wLevel and wLevel > 0 then + lvlCache[wName] = wLevel + end + end + end + PersistClassCache() +end + +function SFrames:GetClassHexForName(name) + if not name or name == "" then return nil end + local cache = self.PlayerClassColorCache + if cache[name] then return cache[name] end + self:RefreshClassColorCache() + return cache[name] +end + +function SFrames:GetLevelForName(name) + if not name or name == "" then return nil end + local cache = self.PlayerLevelCache + if cache[name] then return cache[name] end + return nil +end + +-- 本地别名,供 Chat.lua 内部使用 +local function GetClassHexForName(name) + return SFrames:GetClassHexForName(name) +end + +local function GetLevelForName(name) + return SFrames:GetLevelForName(name) +end + +-- 将聊天文本中所有 |Hplayer:NAME|h[NAME]|h 替换为职业颜色版本,并可选附加等级 +local function ColorPlayerNamesInText(text) + if not text or text == "" then return text end + local showLevel = SFramesDB and SFramesDB.Chat and SFramesDB.Chat.showPlayerLevel ~= false + local result = string.gsub(text, "(|Hplayer:([^|]+)|h%[([^%]]+)%]|h)", function(full, linkName, displayName) + local baseName = string.gsub(linkName, "%-.*", "") + local hex = GetClassHexForName(baseName) + local level = showLevel and GetLevelForName(baseName) or nil + local levelSuffix = "" + if level then + levelSuffix = "|cff888888:" .. level .. "|r" + end + if hex then + return "|Hplayer:" .. linkName .. "|h|cff" .. hex .. "[" .. displayName .. "]|r|h" .. levelSuffix + end + if level then + return full .. levelSuffix + end + return full + end) + return result +end + +local function Trim(text) + text = tostring(text or "") + text = string.gsub(text, "^%s+", "") + text = string.gsub(text, "%s+$", "") + return text +end + +local function Dummy() end + +local function CopyTable(src) + local out = {} + for k, v in pairs(src) do + if type(v) == "table" then + out[k] = CopyTable(v) + else + out[k] = v + end + end + return out +end + +local function BuildDefaultTranslateFilters() + local out = {} + for key in pairs(TRANSLATE_FILTER_KEYS) do + out[key] = false + end + return out +end + +local function ShortText(text, maxLen) + text = tostring(text or "") + local n = maxLen or 10 + if string.len(text) <= n then return text end + return string.sub(text, 1, n - 1) .. "." +end + +local function GetFilterLabel(key) + for i = 1, table.getn(FILTER_DEFS) do + local def = FILTER_DEFS[i] + if def and def.key == key then + return def.label or key + end + end + return key +end + +local function NormalizeChannelName(name) + local clean = Trim(name) + if clean == "" then return "" end + clean = string.gsub(clean, "|Hchannel:[^|]+|h%[([^%]]+)%]|h", "%1") + clean = string.gsub(clean, "^%[", "") + clean = string.gsub(clean, "%]$", "") + clean = string.gsub(clean, "^%d+%s*%.%s*", "") + clean = string.gsub(clean, "^%d+%s*:%s*", "") + clean = Trim(clean) + return clean +end + +local function ChannelKey(name) + local clean = NormalizeChannelName(name) + if clean == "" then return "" end + return string.lower(clean) +end + +-- Groups of channel aliases that should be treated as the same logical channel. +-- If a user enables any one key in a group, all keys in that group are considered enabled. +local CHANNEL_ALIAS_GROUPS = { + { "hc", "hardcore", "hard core", "hard-core", "h", "硬核" }, + { "lfg", "lft", "lookingforgroup", "looking for group", "group" }, +} + +-- Build a reverse lookup: channel key -> alias group index +local CHANNEL_ALIAS_GROUP_INDEX = {} +for gIdx, group in ipairs(CHANNEL_ALIAS_GROUPS) do + for _, alias in ipairs(group) do + CHANNEL_ALIAS_GROUP_INDEX[alias] = gIdx + end +end + +-- Returns all alias keys for the group that `name` belongs to (or nil if not in any group). +local function GetChannelAliasKeys(name) + local key = ChannelKey(name) + if key == "" then return nil end + -- Check exact match first + local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key] + if not gIdx then + -- Check substring match for each alias in each group. + -- Only use aliases 3+ chars long to avoid false positives (e.g. "h" matching "whisper"). + for i, group in ipairs(CHANNEL_ALIAS_GROUPS) do + for _, alias in ipairs(group) do + if string.len(alias) >= 3 and string.find(key, alias, 1, true) then + gIdx = i + break + end + end + if gIdx then break end + end + end + if not gIdx then return nil end + return CHANNEL_ALIAS_GROUPS[gIdx] +end + +local function IsIgnoredChannelByDefault(name) + local key = ChannelKey(name) + if key == "" then return false end + -- Check via alias groups + local aliases = GetChannelAliasKeys(name) + if aliases then return true end + return false +end + +-- Channels discovered at runtime from actual chat messages / join events. +-- Keys are normalized names, values are true. +local discoveredChannels = {} + +local function TrackDiscoveredChannel(name) + if type(name) ~= "string" or name == "" then return end + local clean = NormalizeChannelName(name) + if clean ~= "" then + discoveredChannels[clean] = true + end +end + +local function UntrackDiscoveredChannel(name) + if type(name) ~= "string" or name == "" then return end + local clean = NormalizeChannelName(name) + if clean ~= "" then + discoveredChannels[clean] = nil + end +end + +local function GetJoinedChannels() + local out = {} + if not GetChannelList then return out end + + local raw = { GetChannelList() } + local i = 1 + local seen = {} + while i <= table.getn(raw) do + local id = raw[i] + local name = raw[i + 1] + if type(id) == "number" and type(name) == "string" and Trim(name) ~= "" then + table.insert(out, { id = id, name = name }) + local key = ChannelKey(name) + if key ~= "" then + seen[key] = true + end + end + i = i + 3 + end + + table.sort(out, function(a, b) + if a.id == b.id then + return string.lower(a.name) < string.lower(b.name) + end + return a.id < b.id + end) + + -- Add one representative per alias group that isn't already present, + -- plus individual channels that are not aliases. + -- For alias groups (like hc/hardcore/硬核), only add ONE representative + -- so we don't create duplicate conflicting entries. + local customChannels = { "hc", "硬核", "hardcore", "h", "交易", "综合", "世界防务", "本地防务", "world" } + local seenAliasGroups = {} + for _, cname in ipairs(customChannels) do + local key = ChannelKey(cname) + if key == "" then + -- skip + elseif seen[key] then + -- already in the joined list + local aliases = GetChannelAliasKeys(cname) + if aliases then + -- Mark the whole group as seen so no other alias gets added + local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key] + if gIdx then seenAliasGroups[gIdx] = true end + end + else + local aliases = GetChannelAliasKeys(cname) + if aliases then + -- This is an alias channel - check if any alias is already seen/added + local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key] + -- Also check substring aliases + if not gIdx then + for i, group in ipairs(CHANNEL_ALIAS_GROUPS) do + for _, a in ipairs(group) do + if string.find(key, a, 1, true) then + gIdx = i + break + end + end + if gIdx then break end + end + end + local alreadyInJoined = false + if gIdx then + for _, a in ipairs(CHANNEL_ALIAS_GROUPS[gIdx]) do + if seen[a] then + alreadyInJoined = true + break + end + end + end + if not alreadyInJoined and (not gIdx or not seenAliasGroups[gIdx]) then + table.insert(out, { id = 99, name = cname }) + seen[key] = true + if gIdx then seenAliasGroups[gIdx] = true end + end + else + table.insert(out, { id = 99, name = cname }) + seen[key] = true + end + end + end + + -- Include dynamically discovered channels (from actual chat messages). + -- Verify each with GetChannelName() to confirm the player is still in it. + for dName, _ in pairs(discoveredChannels) do + local key = ChannelKey(dName) + if key ~= "" and not seen[key] then + local aliasKeys = GetChannelAliasKeys(dName) + local aliasAlreadySeen = false + if aliasKeys then + for _, a in ipairs(aliasKeys) do + if seen[a] then aliasAlreadySeen = true; break end + end + end + if not aliasAlreadySeen then + if GetChannelName then + local chId, chRealName = GetChannelName(dName) + if type(chId) == "number" and chId > 0 then + table.insert(out, { id = chId, name = chRealName or dName }) + seen[key] = true + end + else + table.insert(out, { id = 99, name = dName }) + seen[key] = true + end + end + end + end + + return out +end + +local function MatchJoinedChannelName(rawName) + local key = ChannelKey(rawName) + if key == "" then return "" end + + local channels = GetJoinedChannels() + + -- First: exact key match + for i = 1, table.getn(channels) do + local name = channels[i] and channels[i].name + if ChannelKey(name) == key then + return name + end + end + + -- Second: alias-group match (e.g. "Hardcore" -> joined "hc") + local aliases = GetChannelAliasKeys(rawName) + if aliases then + for _, alias in ipairs(aliases) do + local aliasKey = ChannelKey(alias) + if aliasKey ~= "" then + for i = 1, table.getn(channels) do + local name = channels[i] and channels[i].name + if ChannelKey(name) == aliasKey then + return name + end + end + end + end + end + + return NormalizeChannelName(rawName) +end + +local function GetChannelNameFromMessageEvent(channelString, channelBaseName, channelName, fallback) + local candidates = { + channelName, + channelBaseName, + channelString, + arg9, + arg8, + arg4, + fallback, + } + + for i = 1, table.getn(candidates) do + local candidate = candidates[i] + if type(candidate) == "string" and candidate ~= "" then + local matched = MatchJoinedChannelName(candidate) + if matched ~= "" then + return matched + end + end + end + + for i = 1, table.getn(candidates) do + local candidate = candidates[i] + if type(candidate) == "string" and candidate ~= "" then + local normalized = NormalizeChannelName(candidate) + if normalized ~= "" then + return normalized + end + end + end + + return "" +end + +local function GetChannelNameFromChatLine(text) + if type(text) ~= "string" or text == "" then return nil end + + local _, _, label = string.find(text, "|Hchannel:[^|]+|h%[([^%]]+)%]|h") + if not label then + local raw = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") + raw = string.gsub(raw, "|r", "") + raw = string.gsub(raw, "^%s+", "") + _, _, label = string.find(raw, "^%[([^%]]+)%]") + end + if not label or label == "" then return nil end + + label = string.gsub(label, "^%d+%s*%.%s*", "") + return label +end + +local function GetTranslateFilterKeyForEvent(event) + return TRANSLATE_EVENT_FILTERS[event] +end + +local function ParseHardcoreDeathMessage(text) + if type(text) ~= "string" or text == "" then return nil end + local clean = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") + clean = string.gsub(clean, "|r", "") + + -- Check for Hardcore death signatures + if string.find(string.lower(clean), "hc news") or string.find(clean, "硬核") or string.find(clean, "死亡") or string.find(string.lower(clean), "has fallen") or string.find(string.lower(clean), "died") or string.find(string.lower(clean), "slain") then + -- Turtle English "Level 14" + local _, _, lvlStr = string.find(clean, "Level%s+(%d+)") + if lvlStr then return tonumber(lvlStr) end + + -- Chinese "14级" + local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级") + if lvlStr2 then return tonumber(lvlStr2) end + + -- Fallback + local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)") + if lvlStr3 then return tonumber(lvlStr3) end + + -- If it matches death signatures but no level is found, return 1 as a baseline to trigger the filter. + if string.find(string.lower(clean), "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(clean, "has fallen"))) then + return 1 + end + end + return nil +end + +local function CleanTextForTranslation(text) + if type(text) ~= "string" then return "" end + local clean = text + clean = string.gsub(clean, "|c%x%x%x%x%x%x%x%x", "") + clean = string.gsub(clean, "|r", "") + clean = string.gsub(clean, "|H.-|h(.-)|h", "%1") + clean = string.gsub(clean, "^%s+", "") + clean = string.gsub(clean, "%s+$", "") + return clean +end + +local function ForceHide(object) + if not object then return end + object:Hide() + if object.SetAlpha then object:SetAlpha(0) end + if object.EnableMouse then object:EnableMouse(false) end + object.Show = Dummy +end + +local function ForceInvisible(object) + if not object then return end + object:Hide() + if object.SetAlpha then object:SetAlpha(0) end + if object.EnableMouse then object:EnableMouse(false) end +end + +local function CreateFont(parent, size, justify) + if SFrames and SFrames.CreateFontString then + return SFrames:CreateFontString(parent, size, justify) + end + + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont("Fonts\\ARIALN.TTF", size or 11, "OUTLINE") + fs:SetJustifyH(justify or "LEFT") + fs:SetTextColor(1, 1, 1) + return fs +end + +local configWidgetId = 0 + +local function NextConfigWidget(prefix) + configWidgetId = configWidgetId + 1 + return "SFramesChatCfg" .. prefix .. tostring(configWidgetId) +end + +local CFG_THEME = SFrames.ActiveTheme + +local function EnsureCfgBackdrop(frame) + if not frame then return end + if frame.sfCfgBackdrop then return end + if SFrames and SFrames.CreateBackdrop then + SFrames:CreateBackdrop(frame) + elseif frame.SetBackdrop then + frame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + end + frame.sfCfgBackdrop = true +end + +local function HideCfgTexture(tex) + if not tex then return end + if tex.SetTexture then tex:SetTexture(nil) end + tex:Hide() +end + +local function StyleCfgButton(btn) + if not btn or btn.sfCfgStyled then return btn end + btn.sfCfgStyled = true + + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + HideCfgTexture(btn.GetNormalTexture and btn:GetNormalTexture()) + HideCfgTexture(btn.GetPushedTexture and btn:GetPushedTexture()) + HideCfgTexture(btn.GetHighlightTexture and btn:GetHighlightTexture()) + HideCfgTexture(btn.GetDisabledTexture and btn:GetDisabledTexture()) + + local name = btn:GetName() or "" + for _, suffix in ipairs({ "Left", "Right", "Middle" }) do + local tex = _G[name .. suffix] + if tex then tex:SetAlpha(0) tex:Hide() end + end + + local function SetVisual(state) + local bg = CFG_THEME.buttonBg + local border = CFG_THEME.buttonBorder + local text = CFG_THEME.buttonText + if state == "hover" then + bg = CFG_THEME.buttonHoverBg + border = { CFG_THEME.buttonBorder[1] * 1.5, CFG_THEME.buttonBorder[2] * 1.5, CFG_THEME.buttonBorder[3] * 1.5, 0.98 } + text = { 1, 0.92, 0.96 } + elseif state == "down" then + bg = CFG_THEME.buttonDownBg + elseif state == "disabled" then + bg = CFG_THEME.buttonDisabledBg + text = CFG_THEME.buttonDisabledText + end + if btn.SetBackdropColor then + btn:SetBackdropColor(bg[1], bg[2], bg[3], bg[4]) + end + if btn.SetBackdropBorderColor then + btn:SetBackdropBorderColor(border[1], border[2], border[3], border[4]) + end + local fs = btn.GetFontString and btn:GetFontString() + if fs then + fs:SetTextColor(text[1], text[2], text[3]) + end + end + + btn.RefreshVisual = function() + if btn.IsEnabled and not btn:IsEnabled() then + SetVisual("disabled") + else + SetVisual("normal") + end + end + + local oldEnter = btn:GetScript("OnEnter") + local oldLeave = btn:GetScript("OnLeave") + local oldDown = btn:GetScript("OnMouseDown") + local oldUp = btn:GetScript("OnMouseUp") + + btn:SetScript("OnEnter", function() + if oldEnter then oldEnter() end + if this.IsEnabled and this:IsEnabled() then + SetVisual("hover") + end + end) + btn:SetScript("OnLeave", function() + if oldLeave then oldLeave() end + if this.IsEnabled and this:IsEnabled() then + SetVisual("normal") + end + end) + btn:SetScript("OnMouseDown", function() + if oldDown then oldDown() end + if this.IsEnabled and this:IsEnabled() then + SetVisual("down") + end + end) + btn:SetScript("OnMouseUp", function() + if oldUp then oldUp() end + if this.IsEnabled and this:IsEnabled() then + SetVisual("hover") + end + end) + + btn:RefreshVisual() + return btn +end + +local function StyleCfgCheck(cb) + if not cb or cb.sfCfgStyled then return cb end + cb.sfCfgStyled = true + + local box = CreateFrame("Frame", nil, cb) + box:SetPoint("TOPLEFT", cb, "TOPLEFT", 2, -2) + box:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -2, 2) + local boxLevel = (cb:GetFrameLevel() or 1) - 1 + if boxLevel < 0 then boxLevel = 0 end + box:SetFrameLevel(boxLevel) + EnsureCfgBackdrop(box) + if box.SetBackdropColor then + box:SetBackdropColor(CFG_THEME.checkBg[1], CFG_THEME.checkBg[2], CFG_THEME.checkBg[3], CFG_THEME.checkBg[4]) + end + if box.SetBackdropBorderColor then + box:SetBackdropBorderColor(CFG_THEME.checkBorder[1], CFG_THEME.checkBorder[2], CFG_THEME.checkBorder[3], CFG_THEME.checkBorder[4]) + end + cb.sfCfgBox = box + + HideCfgTexture(cb.GetNormalTexture and cb:GetNormalTexture()) + HideCfgTexture(cb.GetPushedTexture and cb:GetPushedTexture()) + HideCfgTexture(cb.GetHighlightTexture and cb:GetHighlightTexture()) + HideCfgTexture(cb.GetDisabledTexture and cb:GetDisabledTexture()) + + if cb.SetCheckedTexture then + cb:SetCheckedTexture("Interface\\Buttons\\WHITE8X8") + end + local checked = cb.GetCheckedTexture and cb:GetCheckedTexture() + if checked then + checked:ClearAllPoints() + checked:SetPoint("TOPLEFT", cb, "TOPLEFT", 5, -5) + checked:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -5, 5) + if checked.SetDrawLayer then + checked:SetDrawLayer("OVERLAY", 7) + end + if checked.SetAlpha then + checked:SetAlpha(1) + end + checked:SetVertexColor(CFG_THEME.checkFill[1], CFG_THEME.checkFill[2], CFG_THEME.checkFill[3], CFG_THEME.checkFill[4]) + end + + if cb.SetDisabledCheckedTexture then + cb:SetDisabledCheckedTexture("Interface\\Buttons\\WHITE8X8") + end + local disChecked = cb.GetDisabledCheckedTexture and cb:GetDisabledCheckedTexture() + if disChecked then + disChecked:ClearAllPoints() + disChecked:SetPoint("TOPLEFT", cb, "TOPLEFT", 5, -5) + disChecked:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -5, 5) + if disChecked.SetDrawLayer then + disChecked:SetDrawLayer("OVERLAY", 6) + end + disChecked:SetVertexColor(0.5, 0.5, 0.55, 0.8) + end + + local label = _G[cb:GetName() .. "Text"] + if label then + label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + end + + local oldEnter = cb:GetScript("OnEnter") + local oldLeave = cb:GetScript("OnLeave") + cb:SetScript("OnEnter", function() + if oldEnter then oldEnter() end + if this.sfCfgBox and this.sfCfgBox.SetBackdropBorderColor then + this.sfCfgBox:SetBackdropBorderColor(CFG_THEME.checkHoverBorder[1], CFG_THEME.checkHoverBorder[2], CFG_THEME.checkHoverBorder[3], CFG_THEME.checkHoverBorder[4]) + end + end) + cb:SetScript("OnLeave", function() + if oldLeave then oldLeave() end + if this.sfCfgBox and this.sfCfgBox.SetBackdropBorderColor then + this.sfCfgBox:SetBackdropBorderColor(CFG_THEME.checkBorder[1], CFG_THEME.checkBorder[2], CFG_THEME.checkBorder[3], CFG_THEME.checkBorder[4]) + end + end) + return cb +end + +local function StyleCfgSlider(slider, low, high, text) + if not slider or slider.sfCfgStyled then return slider end + slider.sfCfgStyled = true + + local regions = { slider:GetRegions() } + for i = 1, table.getn(regions) do + local region = regions[i] + if region and region.GetObjectType and region:GetObjectType() == "Texture" then + region:SetTexture(nil) + end + end + + local track = slider:CreateTexture(nil, "BACKGROUND") + track:SetTexture("Interface\\Buttons\\WHITE8X8") + track:SetPoint("LEFT", slider, "LEFT", 0, 0) + track:SetPoint("RIGHT", slider, "RIGHT", 0, 0) + track:SetHeight(4) + track:SetVertexColor(CFG_THEME.sliderTrack[1], CFG_THEME.sliderTrack[2], CFG_THEME.sliderTrack[3], CFG_THEME.sliderTrack[4]) + slider.sfTrack = track + + local fill = slider:CreateTexture(nil, "ARTWORK") + fill:SetTexture("Interface\\Buttons\\WHITE8X8") + fill:SetPoint("LEFT", track, "LEFT", 0, 0) + fill:SetPoint("TOP", track, "TOP", 0, 0) + fill:SetPoint("BOTTOM", track, "BOTTOM", 0, 0) + fill:SetWidth(1) + fill:SetVertexColor(CFG_THEME.sliderFill[1], CFG_THEME.sliderFill[2], CFG_THEME.sliderFill[3], CFG_THEME.sliderFill[4]) + slider.sfFill = fill + + if slider.SetThumbTexture then + slider:SetThumbTexture("Interface\\Buttons\\WHITE8X8") + end + local thumb = slider.GetThumbTexture and slider:GetThumbTexture() + if thumb then + thumb:SetWidth(8) + thumb:SetHeight(16) + thumb:SetVertexColor(CFG_THEME.sliderThumb[1], CFG_THEME.sliderThumb[2], CFG_THEME.sliderThumb[3], CFG_THEME.sliderThumb[4]) + end + + local function UpdateFill() + local minValue, maxValue = slider:GetMinMaxValues() + local value = slider:GetValue() or minValue + local pct = 0 + if maxValue > minValue then + pct = (value - minValue) / (maxValue - minValue) + end + pct = Clamp(pct, 0, 1) + local width = math.floor((slider:GetWidth() or 1) * pct + 0.5) + if width < 1 then width = 1 end + slider.sfFill:SetWidth(width) + end + + local oldChanged = slider:GetScript("OnValueChanged") + slider:SetScript("OnValueChanged", function() + if oldChanged then oldChanged() end + UpdateFill() + end) + UpdateFill() + + if low then + low:SetTextColor(0.74, 0.72, 0.8) + low:ClearAllPoints() + low:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 0, 0) + end + if high then + high:SetTextColor(0.74, 0.72, 0.8) + high:ClearAllPoints() + high:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", 0, 0) + end + if text then + text:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + text:ClearAllPoints() + text:SetPoint("BOTTOM", slider, "TOP", 0, 2) + end + return slider +end + +local function StyleCfgEditBox(eb) + if not eb or eb.sfCfgStyled then return eb end + eb.sfCfgStyled = true + + local name = eb:GetName() + if name and name ~= "" then + HideCfgTexture(_G[name .. "Left"]) + HideCfgTexture(_G[name .. "Middle"]) + HideCfgTexture(_G[name .. "Right"]) + end + + local bg = CreateFrame("Frame", nil, eb) + bg:SetPoint("TOPLEFT", eb, "TOPLEFT", -3, 3) + bg:SetPoint("BOTTOMRIGHT", eb, "BOTTOMRIGHT", 3, -3) + bg:SetFrameLevel((eb:GetFrameLevel() or 1) - 1) + EnsureCfgBackdrop(bg) + if bg.SetBackdropColor then + bg:SetBackdropColor(0.13, 0.14, 0.17, 0.94) + end + if bg.SetBackdropBorderColor then + bg:SetBackdropBorderColor(0.52, 0.5, 0.58, 0.88) + end + eb.sfCfgBg = bg + if eb.SetTextInsets then + eb:SetTextInsets(4, 4, 0, 0) + end + if eb.SetTextColor then + eb:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + end + return eb +end + +local function CreateCfgSection(parent, title, x, y, width, height, font) + local sec = CreateFrame("Frame", NextConfigWidget("Section"), parent) + sec:SetWidth(width) + sec:SetHeight(height) + sec:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + + EnsureCfgBackdrop(sec) + if sec.SetBackdropColor then + sec:SetBackdropColor(CFG_THEME.sectionBg[1], CFG_THEME.sectionBg[2], CFG_THEME.sectionBg[3], CFG_THEME.sectionBg[4]) + end + if sec.SetBackdropBorderColor then + sec:SetBackdropBorderColor(CFG_THEME.sectionBorder[1], CFG_THEME.sectionBorder[2], CFG_THEME.sectionBorder[3], CFG_THEME.sectionBorder[4]) + end + + local header = sec:CreateFontString(nil, "OVERLAY") + header:SetFont(font or "Fonts\\ARIALN.TTF", 11, "OUTLINE") + header:SetPoint("TOPLEFT", sec, "TOPLEFT", 8, -8) + header:SetText(title or "") + header:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) + + local div = sec:CreateTexture(nil, "ARTWORK") + div:SetTexture("Interface\\Buttons\\WHITE8X8") + div:SetHeight(1) + div:SetPoint("TOPLEFT", sec, "TOPLEFT", 8, -24) + div:SetPoint("TOPRIGHT", sec, "TOPRIGHT", -8, -24) + div:SetVertexColor(CFG_THEME.sectionBorder[1], CFG_THEME.sectionBorder[2], CFG_THEME.sectionBorder[3], 0.6) + + return sec +end + +local function CreateCfgButton(parent, text, x, y, width, height, onClick) + local btn = CreateFrame("Button", NextConfigWidget("Button"), parent, "UIPanelButtonTemplate") + btn:SetWidth(width) + btn:SetHeight(height) + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + btn:SetText(text or "") + btn:SetScript("OnClick", onClick) + StyleCfgButton(btn) + return btn +end + +local function AddBtnIcon(btn, iconKey, size, align) + if not (SFrames and SFrames.CreateIcon) then return end + local sz = size or 12 + local gap = 3 + local ico = SFrames:CreateIcon(btn, iconKey, sz) + ico:SetDrawLayer("OVERLAY") + btn.nanamiIcon = ico + local fs = btn:GetFontString() + if fs then + fs:ClearAllPoints() + if align == "left" then + ico:SetPoint("LEFT", btn, "LEFT", 8, 0) + fs:SetPoint("LEFT", ico, "RIGHT", gap, 0) + fs:SetPoint("RIGHT", btn, "RIGHT", -4, 0) + fs:SetJustifyH("LEFT") + else + fs:SetPoint("CENTER", btn, "CENTER", (sz + gap) / 2, 0) + ico:SetPoint("RIGHT", fs, "LEFT", -gap, 0) + end + else + ico:SetPoint("CENTER", btn, "CENTER", 0, 0) + end +end + +local function CreateCfgCheck(parent, text, x, y, getter, setter, onChanged) + local cb = CreateFrame("CheckButton", NextConfigWidget("Check"), parent, "UICheckButtonTemplate") + cb:SetWidth(20) + cb:SetHeight(20) + cb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + StyleCfgCheck(cb) + local label = _G[cb:GetName() .. "Text"] + if label then + label:SetText(text or "") + label:SetJustifyH("LEFT") + end + + local internal = false + cb:SetScript("OnClick", function() + if internal then return end + local checked = (this:GetChecked() and true or false) + if setter then setter(checked) end + if onChanged then onChanged(checked) end + end) + + cb.Refresh = function() + internal = true + cb:SetChecked(getter and getter() and true or false) + internal = false + end + + cb:Refresh() + return cb +end + +local function CreateCfgSlider(parent, labelText, x, y, width, minValue, maxValue, step, getter, setter, formatter, onChanged) + local sliderName = NextConfigWidget("Slider") + local slider = CreateFrame("Slider", sliderName, parent, "OptionsSliderTemplate") + slider:SetWidth(width) + slider:SetHeight(26) + slider:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + slider:SetMinMaxValues(minValue, maxValue) + slider:SetValueStep(step) + if slider.SetObeyStepOnDrag then slider:SetObeyStepOnDrag(true) end + + local low = _G[sliderName .. "Low"] + local high = _G[sliderName .. "High"] + local text = _G[sliderName .. "Text"] + if low then low:SetText(tostring(minValue)) end + if high then high:SetText(tostring(maxValue)) end + + local internal = false + local function UpdateLabel(value) + local display = formatter and formatter(value) or value + if text then text:SetText((labelText or "") .. ": " .. tostring(display)) end + end + + slider:SetScript("OnValueChanged", function() + if internal then return end + local raw = this:GetValue() or minValue or 0 + local safeStep = step or 1 + local value + if safeStep >= 1 then + value = math.floor(raw + 0.5) + else + if safeStep == 0 then safeStep = 1 end + value = math.floor(raw / safeStep + 0.5) * safeStep + end + value = Clamp(value, minValue, maxValue) + if setter then setter(value) end + UpdateLabel(value) + if onChanged then onChanged(value) end + end) + + slider.Refresh = function() + local value = tonumber(getter and getter() or minValue) or minValue + value = Clamp(value, minValue, maxValue) + internal = true + slider:SetValue(value) + internal = false + UpdateLabel(value) + end + + StyleCfgSlider(slider, low, high, text) + slider:Refresh() + return slider +end + +local function CreateCfgEditBox(parent, x, y, width, height, getter, setter) + local eb = CreateFrame("EditBox", NextConfigWidget("Edit"), parent, "InputBoxTemplate") + eb:SetWidth(width) + eb:SetHeight(height) + eb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + eb:SetAutoFocus(false) + StyleCfgEditBox(eb) + eb:SetScript("OnEnterPressed", function() + if setter then setter(this:GetText() or "") end + this:ClearFocus() + end) + eb:SetScript("OnEscapePressed", function() + this:ClearFocus() + if getter then this:SetText(getter() or "") end + end) + eb.Refresh = function() + if getter then eb:SetText(getter() or "") end + end + eb:Refresh() + return eb +end + +local function BuildDefaultTab(id, name) + return { + id = id, + name = name or ("Tab" .. tostring(id)), + filters = CopyTable(DEFAULT_FILTERS), + channelFilters = {}, + translateFilters = BuildDefaultTranslateFilters(), + channelTranslateFilters = {}, + } +end + +local function BuildCombatTab(id, name) + local tab = BuildDefaultTab(id, name or DEFAULT_COMBAT_TAB_NAME) + tab.kind = "combat" + tab.filters.say = false + tab.filters.yell = false + tab.filters.emote = false + tab.filters.guild = false + tab.filters.party = false + tab.filters.raid = false + tab.filters.whisper = false + tab.filters.channel = false + tab.filters.system = true + tab.filters.loot = true + tab.filters.money = true + return tab +end + +local function IsCombatTab(tab) + if type(tab) ~= "table" then return false end + if tab.kind == "combat" then return true end + + local nameKey = ChannelKey(tab.name) + if nameKey == "combat" then return true end + if nameKey == "combat log" then return true end + + local localizedKey = ChannelKey(DEFAULT_COMBAT_TAB_NAME) + if localizedKey ~= "" and nameKey == localizedKey then return true end + return false +end + +local function SanitizeTab(tab, fallbackId, fallbackName) + if type(tab) ~= "table" then tab = {} end + if type(tab.id) ~= "number" then tab.id = fallbackId or 1 end + if type(tab.kind) ~= "string" then tab.kind = nil end + + if type(tab.name) ~= "string" or tab.name == "" then + tab.name = fallbackName or ("Tab" .. tostring(tab.id)) + else + tab.name = Trim(tab.name) + if tab.name == "" then + tab.name = fallbackName or ("Tab" .. tostring(tab.id)) + end + end + + if type(tab.filters) ~= "table" then tab.filters = {} end + for key, defaultValue in pairs(DEFAULT_FILTERS) do + if tab.filters[key] == nil then + tab.filters[key] = defaultValue + else + tab.filters[key] = (tab.filters[key] == true) + end + end + + if type(tab.channelFilters) ~= "table" then tab.channelFilters = {} end + -- Pass 1: normalize all keys and collect alias group values + local cfGroupValues = {} -- gIdx -> value (last-write wins: true beats false) + for key, value in pairs(tab.channelFilters) do + if type(key) ~= "string" then + tab.channelFilters[key] = nil + else + local normalized = ChannelKey(key) + if normalized == "" then + tab.channelFilters[key] = nil + else + local aliases = GetChannelAliasKeys(normalized) + if aliases then + -- Find this key's group index + local gIdx = CHANNEL_ALIAS_GROUP_INDEX[normalized] + if not gIdx then + for gi, group in ipairs(CHANNEL_ALIAS_GROUPS) do + for _, a in ipairs(group) do + if string.find(normalized, a, 1, true) then gIdx = gi break end + end + if gIdx then break end + end + end + if gIdx then + local enabled = (value == true) + -- true overrides false (if any alias is enabled, keep enabled) + if cfGroupValues[gIdx] == nil or enabled then + cfGroupValues[gIdx] = enabled + end + end + tab.channelFilters[key] = nil + else + local enabled = (value == true) + if normalized ~= key then + tab.channelFilters[key] = nil + tab.channelFilters[normalized] = enabled + else + tab.channelFilters[key] = enabled + end + end + end + end + end + -- Pass 2: write canonical key for each alias group that has a saved value + for gi, enabled in pairs(cfGroupValues) do + local group = CHANNEL_ALIAS_GROUPS[gi] + if group then + local canonicalKey = ChannelKey(group[1] or "") + if canonicalKey ~= "" then + tab.channelFilters[canonicalKey] = enabled + end + end + end + + if type(tab.translateFilters) ~= "table" then tab.translateFilters = {} end + for key in pairs(TRANSLATE_FILTER_KEYS) do + tab.translateFilters[key] = (tab.translateFilters[key] == true) + end + for key, value in pairs(tab.translateFilters) do + if not TRANSLATE_FILTER_KEYS[key] then + tab.translateFilters[key] = nil + else + tab.translateFilters[key] = (value == true) + end + end + + if type(tab.channelTranslateFilters) ~= "table" then tab.channelTranslateFilters = {} end + local ctfGroupValues = {} + for key, value in pairs(tab.channelTranslateFilters) do + if type(key) ~= "string" then + tab.channelTranslateFilters[key] = nil + else + local normalized = ChannelKey(key) + if normalized == "" then + tab.channelTranslateFilters[key] = nil + else + local aliases = GetChannelAliasKeys(normalized) + if aliases then + local gIdx = CHANNEL_ALIAS_GROUP_INDEX[normalized] + if not gIdx then + for gi, group in ipairs(CHANNEL_ALIAS_GROUPS) do + for _, a in ipairs(group) do + if string.find(normalized, a, 1, true) then gIdx = gi break end + end + if gIdx then break end + end + end + if gIdx then + local enabled = (value == true) + if ctfGroupValues[gIdx] == nil or enabled then + ctfGroupValues[gIdx] = enabled + end + end + tab.channelTranslateFilters[key] = nil + else + local enabled = (value == true) + if normalized ~= key then + tab.channelTranslateFilters[key] = nil + tab.channelTranslateFilters[normalized] = enabled + else + tab.channelTranslateFilters[key] = enabled + end + end + end + end + end + for gi, enabled in pairs(ctfGroupValues) do + local group = CHANNEL_ALIAS_GROUPS[gi] + if group then + local canonicalKey = ChannelKey(group[1] or "") + if canonicalKey ~= "" then + tab.channelTranslateFilters[canonicalKey] = enabled + end + end + end + return tab +end + +local function EnsureProtectedTabs(db, maxId) + local tabs = db.tabs + local generalIdx = nil + local combatIdx = nil + + for i = 1, table.getn(tabs) do + if IsCombatTab(tabs[i]) then + if not combatIdx then combatIdx = i end + else + if not generalIdx then generalIdx = i end + end + end + + local function AllocTabId() + local id = db.nextTabId + if type(id) ~= "number" or id <= maxId then + id = maxId + 1 + end + db.nextTabId = id + 1 + maxId = id + return id + end + + if not generalIdx then + local tab = BuildDefaultTab(AllocTabId(), GENERAL or "General") + tab.kind = "general" + table.insert(tabs, 1, tab) + generalIdx = 1 + if combatIdx then combatIdx = combatIdx + 1 end + end + + if not combatIdx then + local tab = BuildCombatTab(AllocTabId(), DEFAULT_COMBAT_TAB_NAME) + table.insert(tabs, tab) + combatIdx = table.getn(tabs) + end + + for i = 1, table.getn(tabs) do + local tab = tabs[i] + if i == generalIdx then + tab.kind = "general" + tab.locked = true + if Trim(tab.name) == "" then tab.name = GENERAL or "General" end + elseif i == combatIdx then + tab.kind = "combat" + tab.locked = true + if Trim(tab.name) == "" then tab.name = DEFAULT_COMBAT_TAB_NAME end + else + if tab.kind == "general" or tab.kind == "combat" then tab.kind = nil end + tab.locked = nil + end + end + + return maxId +end + +local function EnsureDB() + if not SFramesDB then SFramesDB = {} end + if type(SFramesDB.Chat) ~= "table" then SFramesDB.Chat = {} end + + local db = SFramesDB.Chat + if db.enable == nil then db.enable = DEFAULTS.enable end + if db.showBorder == nil then db.showBorder = DEFAULTS.showBorder end + if db.borderClassColor == nil then db.borderClassColor = DEFAULTS.borderClassColor end + if db.showPlayerLevel == nil then db.showPlayerLevel = DEFAULTS.showPlayerLevel end + if type(db.width) ~= "number" then db.width = DEFAULTS.width end + if type(db.height) ~= "number" then db.height = DEFAULTS.height end + if type(db.scale) ~= "number" then db.scale = DEFAULTS.scale end + if type(db.fontSize) ~= "number" then db.fontSize = DEFAULTS.fontSize end + if type(db.sidePadding) ~= "number" then db.sidePadding = DEFAULTS.sidePadding end + if type(db.topPadding) ~= "number" then db.topPadding = DEFAULTS.topPadding end + if type(db.bottomPadding) ~= "number" then db.bottomPadding = DEFAULTS.bottomPadding end + if type(db.bgAlpha) ~= "number" then db.bgAlpha = DEFAULTS.bgAlpha end + if type(db.editBoxPosition) ~= "string" then db.editBoxPosition = DEFAULTS.editBoxPosition end + if type(db.editBoxX) ~= "number" then db.editBoxX = DEFAULTS.editBoxX end + if type(db.editBoxY) ~= "number" then db.editBoxY = DEFAULTS.editBoxY end + if type(db.layoutVersion) ~= "number" then db.layoutVersion = 1 end + if db.layoutVersion < 2 then + db.topPadding = DEFAULTS.topPadding + db.layoutVersion = 2 + end + if db.layoutVersion < 3 then + db.showBorder = false + db.layoutVersion = 3 + end + + if type(db.tabs) ~= "table" or table.getn(db.tabs) == 0 then + db.tabs = { + BuildDefaultTab(1, GENERAL or "General"), + BuildCombatTab(2, DEFAULT_COMBAT_TAB_NAME), + } + end + + local maxId = 0 + for i = 1, table.getn(db.tabs) do + db.tabs[i] = SanitizeTab(db.tabs[i], i, "Tab" .. tostring(i)) + if IsCombatTab(db.tabs[i]) then + db.tabs[i].kind = "combat" + end + if db.tabs[i].id > maxId then maxId = db.tabs[i].id end + end + + if type(db.nextTabId) ~= "number" or db.nextTabId <= maxId then + db.nextTabId = maxId + 1 + end + + if db.layoutVersion < 4 then + local hasCombat = false + for i = 1, table.getn(db.tabs) do + if IsCombatTab(db.tabs[i]) then + hasCombat = true + break + end + end + + if not hasCombat then + local id = db.nextTabId + if type(id) ~= "number" or id <= maxId then + id = maxId + 1 + end + table.insert(db.tabs, BuildCombatTab(id, DEFAULT_COMBAT_TAB_NAME)) + db.nextTabId = id + 1 + end + + db.layoutVersion = 4 + end + + if type(db.activeTab) ~= "number" then db.activeTab = DEFAULTS.activeTab end + db.activeTab = Clamp(math.floor(db.activeTab + 0.5), 1, table.getn(db.tabs)) + + if db.layoutVersion < 5 then + local activeTab = db.tabs[db.activeTab] + if IsCombatTab(activeTab) then + for i = 1, table.getn(db.tabs) do + if not IsCombatTab(db.tabs[i]) then + db.activeTab = i + break + end + end + end + db.layoutVersion = 5 + end + + maxId = EnsureProtectedTabs(db, maxId) + if type(db.nextTabId) ~= "number" or db.nextTabId <= maxId then + db.nextTabId = maxId + 1 + end + if db.layoutVersion < 6 then + db.layoutVersion = 6 + end + + db.activeTab = Clamp(math.floor((db.activeTab or 1) + 0.5), 1, table.getn(db.tabs)) + return db +end + +local popupFrameCache = {} + +local function RememberPopupFrame(whichKey, popup) + if type(whichKey) ~= "string" or whichKey == "" then return end + if type(popup) ~= "table" then return end + popupFrameCache[whichKey] = popup +end + +local function GetPopupEditBox(popup) + if not popup then return nil end + if popup.editBox then return popup.editBox end + if popup.GetName then + local eb = _G[popup:GetName() .. "EditBox"] + if eb then return eb end + end + return nil +end + +local function GetPopupEditText(popup) + local eb = GetPopupEditBox(popup) + if eb and eb.GetText then + return eb:GetText() or "" + end + return "" +end + +local function SetPopupEditText(popup, text) + local eb = GetPopupEditBox(popup) + if eb and eb.SetText then + eb:SetText(text or "") + end +end + +local function FocusPopupEdit(popup) + local eb = GetPopupEditBox(popup) + if not eb then return end + if eb.SetFocus then eb:SetFocus() end + if eb.HighlightText then eb:HighlightText() end +end + +local function ResolvePopupFrame(whichKey, dialog) + if dialog and dialog.GetParent then + local parent = dialog:GetParent() + if parent and parent.which == whichKey and GetPopupEditBox(parent) then + RememberPopupFrame(whichKey, parent) + return parent + end + end + + if dialog and dialog.editBox then + RememberPopupFrame(whichKey, dialog) + return dialog + end + + if this then + if this.editBox then + RememberPopupFrame(whichKey, this) + return this + end + + if this.GetParent then + local parent = this:GetParent() + if parent and parent.which == whichKey and GetPopupEditBox(parent) then + RememberPopupFrame(whichKey, parent) + return parent + end + end + end + + local cached = popupFrameCache[whichKey] + if cached and cached.which == whichKey and GetPopupEditBox(cached) then + return cached + end + + if StaticPopup_FindVisible then + local popup = StaticPopup_FindVisible(whichKey) + if popup then + RememberPopupFrame(whichKey, popup) + return popup + end + end + + for i = 1, 4 do + local popup = _G["StaticPopup" .. tostring(i)] + if popup and popup.which == whichKey then + RememberPopupFrame(whichKey, popup) + return popup + end + end + + return dialog +end + +local function EnsurePopupDialogs() + if not StaticPopupDialogs then return end + + if not StaticPopupDialogs["SFRAMES_CHAT_NEW_TAB"] then + StaticPopupDialogs["SFRAMES_CHAT_NEW_TAB"] = { + text = "请输入新标签标题", + button1 = ACCEPT or "确定", + button2 = CANCEL or "取消", + hasEditBox = 1, + maxLetters = 32, + timeout = 0, + whileDead = 1, + hideOnEscape = 1, + preferredIndex = 3, + OnShow = function(dialog) + local popup = ResolvePopupFrame("SFRAMES_CHAT_NEW_TAB", dialog) + if not popup then return end + local suggested = "Tab" + if SFrames and SFrames.Chat and SFrames.Chat.GetNextTabName then + suggested = SFrames.Chat:GetNextTabName() + end + SetPopupEditText(popup, suggested) + FocusPopupEdit(popup) + end, + OnAccept = function(dialog) + local popup = ResolvePopupFrame("SFRAMES_CHAT_NEW_TAB", dialog) + local text = "" + if popup then + text = Trim(GetPopupEditText(popup)) + end + if text == "" then + if SFrames and SFrames.Chat and SFrames.Chat.GetNextTabName then + text = SFrames.Chat:GetNextTabName() + else + text = "Tab" + end + end + if SFrames and SFrames.Chat then + SFrames.Chat:AddTab(text) + end + end, + } + end + + if not StaticPopupDialogs["SFRAMES_CHAT_RENAME_TAB"] then + StaticPopupDialogs["SFRAMES_CHAT_RENAME_TAB"] = { + text = "重命名标签", + button1 = ACCEPT or "确定", + button2 = CANCEL or "取消", + hasEditBox = 1, + maxLetters = 32, + timeout = 0, + whileDead = 1, + hideOnEscape = 1, + preferredIndex = 3, + OnShow = function(dialog, data) + local popup = ResolvePopupFrame("SFRAMES_CHAT_RENAME_TAB", dialog) + if not popup then return end + local idx = tonumber(data or (popup and popup.data) or (SFrames and SFrames.Chat and SFrames.Chat.pendingRenameIndex)) + local name = "" + if SFrames and SFrames.Chat then + local tab = nil + if type(idx) == "number" then + tab = SFrames.Chat:GetTab(idx) + else + tab = SFrames.Chat:GetActiveTab() + end + if tab and tab.name then + name = tab.name + end + end + SetPopupEditText(popup, name) + FocusPopupEdit(popup) + end, + OnAccept = function(dialog, data) + local popup = ResolvePopupFrame("SFRAMES_CHAT_RENAME_TAB", dialog) + if not popup then return end + local idx = tonumber(data or (popup and popup.data) or (SFrames and SFrames.Chat and SFrames.Chat.pendingRenameIndex)) + local text = GetPopupEditText(popup) + if not (SFrames and SFrames.Chat) then return end + if type(idx) == "number" then + SFrames.Chat:RenameTab(idx, text) + else + SFrames.Chat:RenameActiveTab(text) + end + SFrames.Chat.pendingRenameIndex = nil + end, + } + end + + if not StaticPopupDialogs["SFRAMES_CHAT_RELOAD_PROMPT"] then + StaticPopupDialogs["SFRAMES_CHAT_RELOAD_PROMPT"] = { + text = "更改聊天界面的启用状态需要重载界面(Reload UI)。\n确定要现在重载吗?", + button1 = ACCEPT or "确定", + button2 = CANCEL or "取消", + timeout = 0, + whileDead = 1, + hideOnEscape = 1, + OnAccept = function() + ReloadUI() + end, + OnCancel = function() + -- 恢复之前的勾选状态 + local db = SFrames.Chat:EnsureDB() + db.enable = not db.enable + SFrames.Chat:RefreshConfigFrame() + end, + } + end +end + +local function NewDropDownInfo() + if UIDropDownMenu_CreateInfo then + return UIDropDownMenu_CreateInfo() + end + return {} +end + +SFrames.Chat.FilterDefs = FILTER_DEFS + +function SFrames.Chat:EnsureDB() + return EnsureDB() +end + +function SFrames.Chat:GetConfig() + local db = EnsureDB() + local editBoxPosition = tostring(db.editBoxPosition or DEFAULTS.editBoxPosition) + if editBoxPosition ~= "top" and editBoxPosition ~= "bottom" and editBoxPosition ~= "free" then + editBoxPosition = DEFAULTS.editBoxPosition + end + + return { + enable = db.enable ~= false, + showBorder = db.showBorder ~= false, + borderClassColor = db.borderClassColor == true, + width = math.floor(Clamp(db.width, 320, 900) + 0.5), + height = math.floor(Clamp(db.height, 120, 460) + 0.5), + scale = Clamp(db.scale, 0.75, 1.4), + fontSize = math.floor(Clamp(db.fontSize, 10, 18) + 0.5), + sidePadding = math.floor(Clamp(db.sidePadding, 6, 20) + 0.5), + topPadding = math.floor(Clamp(db.topPadding, 24, 64) + 0.5), + bottomPadding = math.floor(Clamp(db.bottomPadding, 4, 18) + 0.5), + bgAlpha = Clamp(db.bgAlpha, 0, 1), + editBoxPosition = editBoxPosition, + editBoxX = tonumber(db.editBoxX) or DEFAULTS.editBoxX, + editBoxY = tonumber(db.editBoxY) or DEFAULTS.editBoxY, + } +end + +function SFrames.Chat:GetTabs() + return EnsureDB().tabs +end + +function SFrames.Chat:GetActiveTabIndex() + local db = EnsureDB() + return Clamp(math.floor((db.activeTab or 1) + 0.5), 1, table.getn(db.tabs)) +end + +function SFrames.Chat:GetActiveTab() + local db = EnsureDB() + return db.tabs[self:GetActiveTabIndex()] +end + +function SFrames.Chat:GetTab(index) + local db = EnsureDB() + index = Clamp(math.floor(tonumber(index) or 1), 1, table.getn(db.tabs)) + return db.tabs[index], index +end + +function SFrames.Chat:GetNextTabName() + local db = EnsureDB() + local id = db.nextTabId or (table.getn(db.tabs) + 1) + return "Tab" .. tostring(id) +end + +function SFrames.Chat:IsTabProtected(index) + local tab = self:GetTab(index) + if not tab then return false end + if tab.locked == true then return true end + if tab.kind == "general" or tab.kind == "combat" then return true end + if IsCombatTab(tab) then return true end + return false +end + +function SFrames.Chat:GetJoinedChannels() + return GetJoinedChannels() +end + +function SFrames.Chat:GetTabChannelFilter(index, channelName) + local tab = self:GetTab(index) + if not tab then return true end + if type(tab.channelFilters) ~= "table" then tab.channelFilters = {} end + local key = ChannelKey(channelName) + if key == "" then return true end + + -- Direct lookup first + local saved = tab.channelFilters[key] + if saved ~= nil then + return saved == true + end + + -- Alias-aware lookup: if this channel belongs to an alias group, + -- check if any alias key has been explicitly saved. + local aliases = GetChannelAliasKeys(channelName) + if aliases then + for _, alias in ipairs(aliases) do + local aliasKey = ChannelKey(alias) + if aliasKey ~= "" and aliasKey ~= key then + local aliasSaved = tab.channelFilters[aliasKey] + if aliasSaved ~= nil then + return aliasSaved == true + end + end + end + -- No explicit save found for any alias; default to blocked + return false + end + + -- Not an ignored channel; default to shown + return true +end + +function SFrames.Chat:GetTabTranslateFilter(index, key) + local tab = self:GetTab(index) + if not tab or not TRANSLATE_FILTER_KEYS[key] then return false end + if type(tab.translateFilters) ~= "table" then + tab.translateFilters = BuildDefaultTranslateFilters() + end + return tab.translateFilters[key] == true +end + +function SFrames.Chat:SetTabTranslateFilter(index, key, enabled) + local tab = self:GetTab(index) + if not tab or not TRANSLATE_FILTER_KEYS[key] then return end + if type(tab.translateFilters) ~= "table" then + tab.translateFilters = BuildDefaultTranslateFilters() + end + tab.translateFilters[key] = (enabled == true) + if self.translateConfigFrame and self.RefreshTranslateConfigFrame then + self:RefreshTranslateConfigFrame() + end + self:NotifyConfigUI() +end + +function SFrames.Chat:SetActiveTabTranslateFilter(key, enabled) + self:SetTabTranslateFilter(self:GetActiveTabIndex(), key, enabled) +end + +function SFrames.Chat:GetTabChannelTranslateFilter(index, channelName) + local tab = self:GetTab(index) + if not tab then return false end + if type(tab.channelTranslateFilters) ~= "table" then tab.channelTranslateFilters = {} end + local key = ChannelKey(channelName) + if key == "" then return false end + if not self:GetTabChannelFilter(index, channelName) then + return false + end + -- Direct lookup + local saved = tab.channelTranslateFilters[key] + if saved ~= nil then return saved == true end + -- Alias-aware lookup + local aliases = GetChannelAliasKeys(channelName) + if aliases then + for _, alias in ipairs(aliases) do + local ak = ChannelKey(alias) + if ak ~= "" and ak ~= key then + local aliasSaved = tab.channelTranslateFilters[ak] + if aliasSaved ~= nil then return aliasSaved == true end + end + end + end + return false +end + +function SFrames.Chat:SetTabChannelTranslateFilter(index, channelName, enabled) + local tab = self:GetTab(index) + if not tab then return end + if type(tab.channelTranslateFilters) ~= "table" then tab.channelTranslateFilters = {} end + local key = ChannelKey(channelName) + if key == "" then return end + -- Save under canonical alias key and clear other alias keys + local canonicalKey = key + local aliases = GetChannelAliasKeys(channelName) + if aliases then + local firstAlias = ChannelKey(aliases[1] or "") + if firstAlias ~= "" then canonicalKey = firstAlias end + for _, alias in ipairs(aliases) do + local ak = ChannelKey(alias) + if ak ~= "" and ak ~= canonicalKey then + tab.channelTranslateFilters[ak] = nil + end + end + end + tab.channelTranslateFilters[canonicalKey] = (enabled == true) + if self.translateConfigFrame and self.RefreshTranslateConfigFrame then + self:RefreshTranslateConfigFrame() + end + self:NotifyConfigUI() +end + +function SFrames.Chat:SetActiveTabChannelTranslateFilter(channelName, enabled) + self:SetTabChannelTranslateFilter(self:GetActiveTabIndex(), channelName, enabled) +end + +function SFrames.Chat:GetTabIndexForChatFrame(frame) + if not frame then return nil end + if self.chatFrameToTabIndex and self.chatFrameToTabIndex[frame] then + return self.chatFrameToTabIndex[frame] + end + local db = EnsureDB() + for i = 1, table.getn(db.tabs) do + local chatFrame = self:GetChatFrameForTab(db.tabs[i]) + if chatFrame == frame then + if not self.chatFrameToTabIndex then self.chatFrameToTabIndex = {} end + self.chatFrameToTabIndex[frame] = i + return i + end + end + return nil +end + +function SFrames.Chat:FindTabIndexById(tabId) + if type(tabId) ~= "number" then return nil end + local db = EnsureDB() + for i = 1, table.getn(db.tabs) do + local tab = db.tabs[i] + if tab and tab.id == tabId then + return i + end + end + return nil +end + +function SFrames.Chat:ShouldAutoTranslateForTab(index, filterKey, channelName) + local tab = self:GetTab(index) + if not tab then return false end + if filterKey == "channel" then + return self:GetTabChannelTranslateFilter(index, channelName) + end + if not TRANSLATE_FILTER_KEYS[filterKey] then + return false + end + if tab.filters and tab.filters[filterKey] == false then + return false + end + if not self:GetTabTranslateFilter(index, filterKey) then + return false + end + return true +end + +function SFrames.Chat:SetTabChannelFilter(index, channelName, enabled) + local tab, idx = self:GetTab(index) + if not tab then return end + if type(tab.channelFilters) ~= "table" then tab.channelFilters = {} end + local key = ChannelKey(channelName) + if key == "" then return end + -- Save under the canonical key only (first alias in the group, or the key itself) + local canonicalKey = key + local aliases = GetChannelAliasKeys(channelName) + if aliases then + -- Use the first alias as the canonical key, but only if it's a simple exact key + local firstAlias = ChannelKey(aliases[1] or "") + if firstAlias ~= "" then canonicalKey = firstAlias end + -- Also clear any previously-saved keys for other aliases in the same group + for _, alias in ipairs(aliases) do + local ak = ChannelKey(alias) + if ak ~= "" and ak ~= canonicalKey then + tab.channelFilters[ak] = nil + end + end + end + tab.channelFilters[canonicalKey] = (enabled == true) + if self:GetActiveTabIndex() == idx then + self:ApplyTabChannels(idx) + end + if self.translateConfigFrame and self.RefreshTranslateConfigFrame then + self:RefreshTranslateConfigFrame() + end + self:NotifyConfigUI() +end + +function SFrames.Chat:SetActiveTabChannelFilter(channelName, enabled) + self:SetTabChannelFilter(self:GetActiveTabIndex(), channelName, enabled) +end + +function SFrames.Chat:NotifyConfigUI() + if SFrames and SFrames.ConfigUI and SFrames.ConfigUI.activePage == "chat" and SFrames.ConfigUI.RefreshChatPage then + SFrames.ConfigUI:RefreshChatPage() + end +end + +function SFrames.Chat:CanUseAutoTranslateAPI() + if SFramesDB and SFramesDB.Chat and SFramesDB.Chat.translateEnabled == false then + return false + end + return _G.STranslateAPI + and _G.STranslateAPI.IsReady + and _G.STranslateAPI.IsReady() + and _G.STranslateAPI.Translate +end + +function SFrames.Chat:RequestAutoTranslation(text, callback) + local cleanText = CleanTextForTranslation(text) + if cleanText == "" then + if callback then callback(nil, "EMPTY_TEXT") end + return false + end + if not self:CanUseAutoTranslateAPI() then + if callback then callback(nil, "API_UNAVAILABLE") end + return false + end + + if not self.translationCache then self.translationCache = {} end + if not self.translationPending then self.translationPending = {} end + + local cacheKey = AUTO_TRANSLATE_TARGET_LANG .. "\031" .. cleanText + local cached = self.translationCache[cacheKey] + if cached and cached ~= "" then + if callback then callback(cached, nil, true) end + return true + end + + local pending = self.translationPending[cacheKey] + if pending then + if callback then table.insert(pending, callback) end + return true + end + + self.translationPending[cacheKey] = {} + if callback then + table.insert(self.translationPending[cacheKey], callback) + end + + local chat = self + _G.STranslateAPI.Translate(cleanText, "auto", AUTO_TRANSLATE_TARGET_LANG, function(result, err, meta) + local callbacks = chat.translationPending and chat.translationPending[cacheKey] + if chat.translationPending then + chat.translationPending[cacheKey] = nil + end + + if result and result ~= "" then + if not chat.translationCache then chat.translationCache = {} end + chat.translationCache[cacheKey] = result + end + + if callbacks then + for i = 1, table.getn(callbacks) do + local cb = callbacks[i] + if cb then + cb(result, err, meta) + end + end + end + end, "Nanami-UI") + return true +end + +function SFrames.Chat:AppendAutoTranslatedLine(tabId, filterKey, channelName, sourceText, translatedText, senderName) + if type(translatedText) ~= "string" or translatedText == "" then return end + + local tabIndex = self:FindTabIndexById(tabId) + if not tabIndex then return end + if not self:ShouldAutoTranslateForTab(tabIndex, filterKey, channelName) then + return + end + + local cleanSource = CleanTextForTranslation(sourceText) + local cleanTranslated = CleanTextForTranslation(translatedText) + if cleanSource == "" or cleanTranslated == "" or cleanSource == cleanTranslated then + return + end + + local tab = self:GetTab(tabIndex) + local chatFrame = self:GetChatFrameForTab(tab) + if not chatFrame then return end + + if not chatFrame.AddMessage then return end + + local prefix = "|cff6ecf6e[AI]|r " + if type(channelName) == "string" and channelName ~= "" then + prefix = prefix .. "|cff8cb4d8[" .. channelName .. "]|r " + end + if type(senderName) == "string" and senderName ~= "" then + prefix = prefix .. "|cffdab777" .. senderName .. ":|r " + end + local fullText = prefix .. "|cffe8dcc8" .. cleanTranslated .. "|r" + chatFrame:AddMessage(fullText) + if type(senderName) == "string" and senderName ~= "" and SFrames.Chat.MessageIndex then + if not SFrames.Chat.MessageSenders then SFrames.Chat.MessageSenders = {} end + SFrames.Chat.MessageSenders[SFrames.Chat.MessageIndex] = senderName + end +end + +function SFrames.Chat:QueueAutoTranslationForFrame(frame, tabIndex, event, messageText, channelName) + if not frame or type(messageText) ~= "string" or messageText == "" then + return + end + + local filterKey = GetTranslateFilterKeyForEvent(event) + if not filterKey then + return + end + if not self:ShouldAutoTranslateForTab(tabIndex, filterKey, channelName) then + return + end + + local tab = self:GetTab(tabIndex) + if not tab or type(tab.id) ~= "number" then + return + end + + local cleanText = CleanTextForTranslation(messageText) + if cleanText == "" then + return + end + + self:RequestAutoTranslation(cleanText, function(result, err, meta) + if result and result ~= "" then + SFrames.Chat:AppendAutoTranslatedLine(tab.id, filterKey, channelName, cleanText, result) + end + end) +end + +function SFrames.Chat:SavePosition() + if not (self.frame and self.frame.GetPoint) then return end + if not SFramesDB then SFramesDB = {} end + if not SFramesDB.Positions then SFramesDB.Positions = {} end + + local point, _, relativePoint, xOfs, yOfs = self.frame:GetPoint() + SFramesDB.Positions["ChatFrame"] = { + point = point, + relativePoint = relativePoint, + xOfs = xOfs, + yOfs = yOfs, + } +end + +function SFrames.Chat:SaveSizeFromFrame() + if not self.frame then return end + local db = EnsureDB() + db.width = math.floor(Clamp(self.frame:GetWidth() or DEFAULTS.width, 320, 900) + 0.5) + db.height = math.floor(Clamp(self.frame:GetHeight() or DEFAULTS.height, 120, 460) + 0.5) + self:NotifyConfigUI() +end + +function SFrames.Chat:ResetPosition() + if not self.frame then return end + if not SFramesDB then SFramesDB = {} end + if not SFramesDB.Positions then SFramesDB.Positions = {} end + SFramesDB.Positions["ChatFrame"] = nil + self.frame:ClearAllPoints() + self.frame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 30, 30) + self:SavePosition() + if SFrames and SFrames.Print then + SFrames:Print("Chat frame position reset.") + end +end + +function SFrames.Chat:SetTabFilter(index, key, enabled) + local tab = self:GetTab(index) + if not tab or tab.filters[key] == nil then return end + tab.filters[key] = (enabled == true) + if self:GetActiveTabIndex() == index then + self:ApplyTabFilters(index) + -- Restore cached messages after filter change cleared the frame + local chatFrame = self:GetChatFrameForTab(tab) + if chatFrame then + self:RestoreCachedMessages(chatFrame) + end + end + self:NotifyConfigUI() +end + +function SFrames.Chat:SetActiveTabFilter(key, enabled) + self:SetTabFilter(self:GetActiveTabIndex(), key, enabled) +end + +function SFrames.Chat:RenameTab(index, name) + local tab, idx = self:GetTab(index) + if not tab then return false end + local clean = Trim(name) + if clean == "" then return false end + tab.name = clean + if self.frame then self:RefreshTabButtons() end + self:NotifyConfigUI() + if idx == self:GetActiveTabIndex() then + self:SwitchActiveChatFrame(self:GetActiveTab()) + end + return true +end + +function SFrames.Chat:RenameActiveTab(name) + return self:RenameTab(self:GetActiveTabIndex(), name) +end + +function SFrames.Chat:PromptNewTab() + EnsurePopupDialogs() + if StaticPopup_Show then + local popup = StaticPopup_Show("SFRAMES_CHAT_NEW_TAB") + if not popup then + local fallbackName = self:GetNextTabName() + self:AddTab(fallbackName) + end + else + self:AddTab(self:GetNextTabName()) + end +end + +function SFrames.Chat:PromptRenameTab(index) + local _, idx = self:GetTab(index or self:GetActiveTabIndex()) + self.pendingRenameIndex = idx + EnsurePopupDialogs() + if StaticPopup_Show then + StaticPopup_Show("SFRAMES_CHAT_RENAME_TAB", nil, nil, idx) + end +end + +function SFrames.Chat:AddTab(name) + local db = EnsureDB() + local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7 + if table.getn(db.tabs) >= maxWindows then + if SFrames and SFrames.Print then SFrames:Print("最多只能创建 " .. maxWindows .. " 个聊天标签。") end + return false + end + + local clean = Trim(name) + if clean == "" then clean = self:GetNextTabName() end + local id = db.nextTabId or (table.getn(db.tabs) + 1) + db.nextTabId = id + 1 + table.insert(db.tabs, BuildDefaultTab(id, clean)) + db.activeTab = table.getn(db.tabs) + + self:RefreshTabButtons() + self:ApplyAllTabsSetup() + self:NotifyConfigUI() + return true +end + +function SFrames.Chat:DeleteTab(index) + local db = EnsureDB() + local tab, idx = self:GetTab(index) + if not tab then return false end + + if self:IsTabProtected(idx) then + if SFrames and SFrames.Print then + SFrames:Print("Default General/Combat tabs cannot be deleted.") + end + return false + end + + if table.getn(db.tabs) <= 2 then + if SFrames and SFrames.Print then + SFrames:Print("Keep at least default General and Combat tabs.") + end + return false + end + + table.remove(db.tabs, idx) + if idx <= db.activeTab then db.activeTab = db.activeTab - 1 end + if db.activeTab < 1 then db.activeTab = 1 end + if db.activeTab > table.getn(db.tabs) then db.activeTab = table.getn(db.tabs) end + + self:RefreshTabButtons() + self:ApplyAllTabsSetup() + self:NotifyConfigUI() + return true +end + +function SFrames.Chat:DeleteActiveTab() + return self:DeleteTab(self:GetActiveTabIndex()) +end + +function SFrames.Chat:OpenFilterConfigForTab(index) + if index then + self:SetActiveTab(index) + end + self:OpenConfigFrame() +end + +function SFrames.Chat:SetActiveTab(index) + local db = EnsureDB() + index = Clamp(math.floor(tonumber(index) or 1), 1, table.getn(db.tabs)) + if db.activeTab == index then return end + db.activeTab = index + self:RefreshTabButtons() + self:SwitchActiveChatFrame(self:GetActiveTab()) + self:NotifyConfigUI() +end + +function SFrames.Chat:StepTab(delta) + local db = EnsureDB() + local count = table.getn(db.tabs) + local target = db.activeTab + (delta or 1) + if target < 1 then target = count end + if target > count then target = 1 end + self:SetActiveTab(target) +end + +function SFrames.Chat:EnsureTabContextMenu() + if self.tabContextMenu then return end + if not (CreateFrame and UIDropDownMenu_Initialize and UIDropDownMenu_AddButton) then return end + + local menu = CreateFrame("Frame", "SFramesChatTabContextMenu", UIParent, "UIDropDownMenuTemplate") + UIDropDownMenu_Initialize(menu, function() + local idx = SFrames.Chat.contextMenuTabIndex or SFrames.Chat:GetActiveTabIndex() + local tab = SFrames.Chat:GetTab(idx) + if not tab then return end + local cfg = SFrames.Chat:GetConfig() + + local info = NewDropDownInfo() + info.text = tab.name or ("标签 " .. tostring(idx)) + info.isTitle = 1 + info.notCheckable = 1 + UIDropDownMenu_AddButton(info) + + info = NewDropDownInfo() + info.text = "重命名标签" + info.notCheckable = 1 + info.func = function() + SFrames.Chat:PromptRenameTab(idx) + end + UIDropDownMenu_AddButton(info) + + info = NewDropDownInfo() + info.text = "删除标签" + info.notCheckable = 1 + info.disabled = (SFrames.Chat:IsTabProtected(idx) or table.getn(SFrames.Chat:GetTabs()) <= 2) and 1 or nil + info.func = function() + SFrames.Chat:DeleteTab(idx) + end + UIDropDownMenu_AddButton(info) + + info = NewDropDownInfo() + info.text = "过滤设置" + info.notCheckable = 1 + info.func = function() + SFrames.Chat:OpenFilterConfigForTab(idx) + end + UIDropDownMenu_AddButton(info) + + info = NewDropDownInfo() + info.text = "当前字号: " .. tostring(cfg.fontSize) + info.notCheckable = 1 + info.disabled = 1 + UIDropDownMenu_AddButton(info) + + info = NewDropDownInfo() + info.text = "增大字号 +1" + info.notCheckable = 1 + info.disabled = (cfg.fontSize >= 18) and 1 or nil + info.func = function() + SFrames.Chat:SetFontSize((SFrames.Chat:GetConfig().fontSize or DEFAULTS.fontSize) + 1) + end + UIDropDownMenu_AddButton(info) + + info = NewDropDownInfo() + info.text = "减小字号 -1" + info.notCheckable = 1 + info.disabled = (cfg.fontSize <= 10) and 1 or nil + info.func = function() + SFrames.Chat:SetFontSize((SFrames.Chat:GetConfig().fontSize or DEFAULTS.fontSize) - 1) + end + UIDropDownMenu_AddButton(info) + end, "MENU") + + self.tabContextMenu = menu +end + +function SFrames.Chat:OpenTabContextMenu(index) + self.contextMenuTabIndex = index + self:EnsureTabContextMenu() + + if self.tabContextMenu and ToggleDropDownMenu then + ToggleDropDownMenu(1, nil, self.tabContextMenu, "cursor", 0, 0) + else + self:PromptRenameTab(index) + end +end + +local function BoolText(v) + if v then return "ON" end + return "OFF" +end + +local function GetPlayerClassColor() + if not UnitClass then return nil end + local _, classFile = UnitClass("player") + if not classFile then return nil end + + local colors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS + if not colors then return nil end + + local classColor = colors[classFile] + if not classColor then return nil end + + local r = classColor.r or classColor[1] + local g = classColor.g or classColor[2] + local b = classColor.b or classColor[3] + if r and g and b then + return r, g, b + end + return nil +end + +function SFrames.Chat:GetBorderColorRGB() + local cfg = self:GetConfig() + if cfg.borderClassColor then + local r, g, b = GetPlayerClassColor() + if r and g and b then + return r, g, b + end + end + return 0.92, 0.56, 0.78 +end + +function SFrames.Chat:SetWindowSize(width, height) + local db = EnsureDB() + db.width = math.floor(Clamp(width or db.width, 320, 900) + 0.5) + db.height = math.floor(Clamp(height or db.height, 120, 460) + 0.5) + self:ApplyConfig() +end + +function SFrames.Chat:SetWindowScale(scale) + local db = EnsureDB() + db.scale = Clamp(scale or db.scale, 0.75, 1.4) + self:ApplyConfig() +end + +function SFrames.Chat:SetFontSize(size) + local db = EnsureDB() + db.fontSize = math.floor(Clamp(size or db.fontSize, 10, 18) + 0.5) + self:ApplyConfig() +end + +function SFrames.Chat:PrintFilters() + local tab = self:GetActiveTab() + if not tab then return end + if SFrames and SFrames.Print then + SFrames:Print("闁荤喐绮庢晶妤呭箰閸涘﹥娅犻柣妯款嚙閸愨偓闂佹悶鍎弲鈺呭礉? " .. tostring(tab.name)) + for i = 1, table.getn(FILTER_DEFS) do + local def = FILTER_DEFS[i] + SFrames:Print(" - " .. def.key .. ": " .. BoolText(tab.filters[def.key] ~= false)) + end + end +end + +function SFrames.Chat:PrintHelp() + if not (SFrames and SFrames.Print) then return end + SFrames:Print("/nui chat (闂備胶鎳撻悘姘跺箰閸濄儲顫曢柟杈鹃檮閸ゅ倸鈹戦悩鎻掆偓鑸电椤栫偞鈷戦柡澶庢硶鑲栭梺?") + SFrames:Print("/nui chat ui") + SFrames:Print("/nui chat size ") + SFrames:Print("/nui chat scale <0.75-1.4>") + SFrames:Print("/nui chat font <10-18>") + SFrames:Print("/nui chat reset") + SFrames:Print("/nui chat tab new ") + SFrames:Print("/nui chat tab del") + SFrames:Print("/nui chat tab next|prev|") + SFrames:Print("/nui chat tab rename ") + SFrames:Print("/nui chat filter on|off") + SFrames:Print("/nui chat filters") +end + +function SFrames.Chat:GetConfigFrameActiveTabName() + local tab = self:GetActiveTab() + if tab and tab.name and tab.name ~= "" then + return tab.name + end + return "Tab" +end + +function SFrames.Chat:RefreshConfigFrame() + if not self.configFrame then return end + + if self.cfgCurrentTabText then + self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName()) + end + + if self.cfgChannelHint then + local channels = self:GetJoinedChannels() + self.cfgChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels))) + end + + if self.cfgRenameBox and self.cfgRenameBox.Refresh then + self.cfgRenameBox:Refresh() + end + + if self.cfgDeleteTabButton then + local protected = self:IsTabProtected(self:GetActiveTabIndex()) + if protected and self.cfgDeleteTabButton.Disable then + self.cfgDeleteTabButton:Disable() + elseif (not protected) and self.cfgDeleteTabButton.Enable then + self.cfgDeleteTabButton:Enable() + end + end + + if self.configControls then + for i = 1, table.getn(self.configControls) do + local ctrl = self.configControls[i] + if ctrl and ctrl.Refresh then + ctrl:Refresh() + end + end + end + + if self.translateConfigFrame and self.RefreshTranslateConfigFrame then + self:RefreshTranslateConfigFrame() + end +end + +function SFrames.Chat:RefreshTranslateConfigFrame() + if not self.translateConfigFrame then return end + + if self.translateCurrentTabText then + self.translateCurrentTabText:SetText("Current Tab: " .. self:GetConfigFrameActiveTabName()) + end + + if self.translateChannelHint then + local channels = self:GetJoinedChannels() + self.translateChannelHint:SetText("Joined Channels: " .. tostring(table.getn(channels))) + end + + if self.translateConfigControls then + for i = 1, table.getn(self.translateConfigControls) do + local ctrl = self.translateConfigControls[i] + if ctrl and ctrl.Refresh then + ctrl:Refresh() + end + end + end +end + +function SFrames.Chat:EnsureTranslateConfigFrame() + if self.translateConfigFrame then return end + + local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + local panel = CreateFrame("Frame", "SFramesChatTranslateConfigPanel", UIParent) + panel:SetWidth(540) + panel:SetHeight(520) + panel:SetPoint("CENTER", UIParent, "CENTER", 180, 0) + panel:SetMovable(true) + panel:EnableMouse(true) + panel:SetClampedToScreen(true) + panel:SetFrameStrata("DIALOG") + panel:SetFrameLevel(151) + panel:RegisterForDrag("LeftButton") + panel:SetScript("OnDragStart", function() this:StartMoving() end) + panel:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + table.insert(UISpecialFrames, "SFramesChatTranslateConfigPanel") + + if SFrames and SFrames.CreateBackdrop then + SFrames:CreateBackdrop(panel) + else + panel:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = true, tileSize = 32, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + end + panel:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], CFG_THEME.panelBg[4]) + panel:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], CFG_THEME.panelBorder[4]) + + local title = panel:CreateFontString(nil, "OVERLAY") + title:SetFont(fontPath, 14, "OUTLINE") + title:SetPoint("TOP", panel, "TOP", 0, -12) + title:SetText("Chat AI Translate") + title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) + + local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -4, -4) + + local controls = {} + local function AddControl(ctrl) + table.insert(controls, ctrl) + end + + local tabSection = CreateCfgSection(panel, "Tab", 10, -36, 520, 92, fontPath) + self.translateCurrentTabText = tabSection:CreateFontString(nil, "OVERLAY") + self.translateCurrentTabText:SetFont(fontPath, 11, "OUTLINE") + self.translateCurrentTabText:SetPoint("TOPLEFT", tabSection, "TOPLEFT", 14, -28) + self.translateCurrentTabText:SetText("Current Tab: " .. self:GetConfigFrameActiveTabName()) + + CreateCfgButton(tabSection, "Prev", 14, -48, 92, 22, function() + SFrames.Chat:StepTab(-1) + SFrames.Chat:RefreshConfigFrame() + SFrames.Chat:RefreshTranslateConfigFrame() + end) + CreateCfgButton(tabSection, "Next", 112, -48, 92, 22, function() + SFrames.Chat:StepTab(1) + SFrames.Chat:RefreshConfigFrame() + SFrames.Chat:RefreshTranslateConfigFrame() + end) + + local filterSection = CreateCfgSection(panel, "Message Translate", 10, -134, 520, 126, fontPath) + for i = 1, table.getn(TRANSLATE_FILTER_ORDER) do + local key = TRANSLATE_FILTER_ORDER[i] + local col = math.mod(i - 1, 2) + local row = math.floor((i - 1) / 2) + local x = 14 + col * 240 + local y = -28 - row * 24 + + AddControl(CreateCfgCheck(filterSection, GetFilterLabel(key), x, y, + function() + return SFrames.Chat:GetTabTranslateFilter(SFrames.Chat:GetActiveTabIndex(), key) + end, + function(checked) + SFrames.Chat:SetActiveTabTranslateFilter(key, checked) + end, + function() + SFrames.Chat:RefreshTranslateConfigFrame() + end + )) + end + + local channelSection = CreateCfgSection(panel, "Channel Translate", 10, -266, 520, 198, fontPath) + self.translateChannelChecks = {} + self.translateChannelHint = channelSection:CreateFontString(nil, "OVERLAY") + self.translateChannelHint:SetFont(fontPath, 10, "OUTLINE") + self.translateChannelHint:SetPoint("BOTTOMLEFT", channelSection, "BOTTOMLEFT", 14, 8) + self.translateChannelHint:SetTextColor(0.84, 0.8, 0.86) + self.translateChannelHint:SetText("Joined Channels:") + + local maxChannelChecks = 15 + for i = 1, maxChannelChecks do + local slot = i + local col = math.mod(i - 1, 3) + local row = math.floor((i - 1) / 3) + local x = 14 + col * 165 + local y = -24 - row * 24 + + local cb = CreateFrame("CheckButton", NextConfigWidget("TranslateChannelCheck"), channelSection, "UICheckButtonTemplate") + cb:SetWidth(20) + cb:SetHeight(20) + cb:SetPoint("TOPLEFT", channelSection, "TOPLEFT", x, y) + StyleCfgCheck(cb) + + local label = _G[cb:GetName() .. "Text"] + if label then + label:SetText("") + label:SetWidth(140) + label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + end + + cb:SetScript("OnClick", function() + local name = this.channelName + if not name or name == "" then return end + SFrames.Chat:SetActiveTabChannelTranslateFilter(name, this:GetChecked() and true or false) + SFrames.Chat:RefreshTranslateConfigFrame() + end) + + cb.Refresh = function() + local channels = SFrames.Chat:GetJoinedChannels() + local info = channels[slot] + if info then + local activeIndex = SFrames.Chat:GetActiveTabIndex() + local enabled = SFrames.Chat:GetTabChannelFilter(activeIndex, info.name) + and SFrames.Chat:GetTabTranslateFilter(activeIndex, "channel") + cb.channelName = info.name + if label then + label:SetText(ShortText(info.name, 24)) + if enabled then + label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + else + label:SetTextColor(0.45, 0.45, 0.45) + end + end + cb:SetChecked(SFrames.Chat:GetTabChannelTranslateFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false) + if enabled then + cb:Enable() + else + cb:Disable() + end + cb:Show() + else + cb.channelName = nil + cb:SetChecked(false) + if label then label:SetText("") end + cb:Hide() + end + end + + AddControl(cb) + table.insert(self.translateChannelChecks, cb) + end + + local tip = channelSection:CreateFontString(nil, "OVERLAY") + tip:SetFont(fontPath, 10, "OUTLINE") + tip:SetPoint("TOPLEFT", channelSection, "TOPLEFT", 14, -150) + tip:SetText("Only active receiving channels can auto-translate.") + tip:SetTextColor(0.7, 0.7, 0.74) + + local close = CreateCfgButton(panel, "Close", 200, -474, 140, 26, function() + SFrames.Chat.translateConfigFrame:Hide() + end) + StyleCfgButton(close) + + self.translateConfigControls = controls + self.translateConfigFrame = panel +end + +function SFrames.Chat:OpenTranslateConfigFrame() + self:EnsureTranslateConfigFrame() + if not self.translateConfigFrame then return end + self:RefreshTranslateConfigFrame() + self.translateConfigFrame:Show() + self.translateConfigFrame:Raise() +end + +function SFrames.Chat:ToggleTranslateConfigFrame() + self:EnsureTranslateConfigFrame() + if not self.translateConfigFrame then return end + if self.translateConfigFrame:IsShown() then + self.translateConfigFrame:Hide() + else + self:OpenTranslateConfigFrame() + end +end + +function SFrames.Chat:EnsureConfigFrame() + if self.configFrame then return end + + local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + local panel = CreateFrame("Frame", "SFramesChatConfigPanel", UIParent) + panel:SetWidth(540) + panel:SetHeight(786) + panel:SetPoint("CENTER", UIParent, "CENTER", 120, 0) + panel:SetMovable(true) + panel:EnableMouse(true) + panel:SetClampedToScreen(true) + panel:SetFrameStrata("DIALOG") + panel:SetFrameLevel(150) + panel:RegisterForDrag("LeftButton") + panel:SetScript("OnDragStart", function() this:StartMoving() end) + panel:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + table.insert(UISpecialFrames, "SFramesChatConfigPanel") + + if SFrames and SFrames.CreateBackdrop then + SFrames:CreateBackdrop(panel) + else + panel:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = true, tileSize = 32, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + end + panel:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], CFG_THEME.panelBg[4]) + panel:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], CFG_THEME.panelBorder[4]) + + local title = panel:CreateFontString(nil, "OVERLAY") + title:SetFont(fontPath, 14, "OUTLINE") + title:SetPoint("TOP", panel, "TOP", 0, -12) + title:SetText("Nanami 聊天设置") + title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) + + local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -4, -4) + + local controls = {} + + local function AddControl(ctrl) + table.insert(controls, ctrl) + end + + local windowSection = CreateCfgSection(panel, "窗口", 10, -36, 520, 226, fontPath) + AddControl(CreateCfgSlider(windowSection, "宽度", 16, -46, 235, 320, 900, 1, + function() return EnsureDB().width end, + function(v) EnsureDB().width = v end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgSlider(windowSection, "高度", 270, -46, 235, 120, 460, 1, + function() return EnsureDB().height end, + function(v) EnsureDB().height = v end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgSlider(windowSection, "缩放", 16, -108, 235, 0.75, 1.40, 0.05, + function() return EnsureDB().scale end, + function(v) EnsureDB().scale = v end, + function(v) return string.format("%.2f", v) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgSlider(windowSection, "字号", 270, -108, 235, 10, 18, 1, + function() return EnsureDB().fontSize end, + function(v) EnsureDB().fontSize = v end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgCheck(windowSection, "启用聊天", 360, -122, + function() return EnsureDB().enable ~= false end, + function(checked) + local oldState = EnsureDB().enable ~= false + if oldState ~= checked then + EnsureDB().enable = (checked == true) + SFrames.Chat:EnsureConfigFrame() + if StaticPopup_Show then + StaticPopup_Show("SFRAMES_CHAT_RELOAD_PROMPT") + else + ReloadUI() + end + end + end, + function() SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgCheck(windowSection, "显示边框", 360, -142, + function() return EnsureDB().showBorder ~= false end, + function(checked) EnsureDB().showBorder = (checked == true) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgCheck(windowSection, "边框职业色", 360, -162, + function() return EnsureDB().borderClassColor == true end, + function(checked) EnsureDB().borderClassColor = (checked == true) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgCheck(windowSection, "显示等级", 360, -182, + function() return EnsureDB().showPlayerLevel ~= false end, + function(checked) EnsureDB().showPlayerLevel = (checked == true) end, + function() SFrames.Chat:RefreshConfigFrame() end + )) + + CreateCfgButton(windowSection, "重置位置", 16, -198, 110, 22, function() + SFrames.Chat:ResetPosition() + end) + CreateCfgButton(windowSection, "解锁", 132, -198, 110, 22, function() + if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end + end) + CreateCfgButton(windowSection, "锁定", 248, -198, 110, 22, function() + if SFrames and SFrames.LockFrames then SFrames:LockFrames() end + end) + + local posLabel = windowSection:CreateFontString(nil, "OVERLAY") + posLabel:SetFont(fontPath, 13, "OUTLINE") + posLabel:SetPoint("TOPLEFT", windowSection, "TOPLEFT", 16, -151) + posLabel:SetText("输入框位置:") + posLabel:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + + CreateCfgButton(windowSection, "底部", 90, -148, 50, 20, function() EnsureDB().editBoxPosition = "bottom"; SFrames.Chat:StyleEditBox() end) + CreateCfgButton(windowSection, "顶部", 144, -148, 50, 20, function() EnsureDB().editBoxPosition = "top"; SFrames.Chat:StyleEditBox() end) + CreateCfgButton(windowSection, "自由拖动 (Alt)", 198, -148, 100, 20, function() + EnsureDB().editBoxPosition = "free" + SFrames.Chat:StyleEditBox() + DEFAULT_CHAT_FRAME:AddMessage("|cffffd100Nanami-UI:|r 按住 Alt 键可以自由拖动输入框。") + end) + + local tabSection = CreateCfgSection(panel, "标签", 10, -268, 520, 118, fontPath) + self.cfgCurrentTabText = tabSection:CreateFontString(nil, "OVERLAY") + self.cfgCurrentTabText:SetFont(fontPath, 11, "OUTLINE") + self.cfgCurrentTabText:SetPoint("TOPLEFT", tabSection, "TOPLEFT", 14, -28) + self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName()) + self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName()) + + CreateCfgButton(tabSection, "上一个", 14, -48, 92, 22, function() + SFrames.Chat:StepTab(-1) + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(tabSection, "下一个", 112, -48, 92, 22, function() + SFrames.Chat:StepTab(1) + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(tabSection, "新建", 210, -48, 92, 22, function() + SFrames.Chat:PromptNewTab() + SFrames.Chat:RefreshConfigFrame() + end) + self.cfgDeleteTabButton = CreateCfgButton(tabSection, "删除", 308, -48, 92, 22, function() + SFrames.Chat:DeleteActiveTab() + SFrames.Chat:RefreshConfigFrame() + end) + + CreateCfgButton(tabSection, "AI", 406, -48, 92, 22, function() + SFrames.Chat:OpenTranslateConfigFrame() + end) + + self.cfgRenameBox = CreateCfgEditBox(tabSection, 14, -80, 188, 20, + function() return SFrames.Chat:GetConfigFrameActiveTabName() end, + function(text) + SFrames.Chat:RenameActiveTab(text) + SFrames.Chat:RefreshConfigFrame() + end + ) + CreateCfgButton(tabSection, "重命名", 210, -80, 92, 22, function() + SFrames.Chat:RenameActiveTab(SFrames.Chat.cfgRenameBox:GetText() or "") + SFrames.Chat:RefreshConfigFrame() + end) + + local filterSection = CreateCfgSection(panel, "消息过滤(当前标签)", 10, -392, 520, 186, fontPath) + for i = 1, table.getn(FILTER_DEFS) do + local def = FILTER_DEFS[i] + local col = math.mod(i - 1, 2) + local row = math.floor((i - 1) / 2) + local x = 14 + col * 240 + local y = -28 - row * 24 + + AddControl(CreateCfgCheck(filterSection, def.label, x, y, + function() + local tab = SFrames.Chat:GetActiveTab() + return tab and tab.filters and tab.filters[def.key] ~= false + end, + function(checked) + SFrames.Chat:SetActiveTabFilter(def.key, checked) + end, + function() + SFrames.Chat:ApplyConfig() + SFrames.Chat:RefreshConfigFrame() + end + )) + end + + local channelSection = CreateCfgSection(panel, "频道过滤(当前标签)", 10, -584, 520, 172, fontPath) + self.cfgChannelChecks = {} + self.cfgChannelHint = channelSection:CreateFontString(nil, "OVERLAY") + self.cfgChannelHint:SetFont(fontPath, 10, "OUTLINE") + self.cfgChannelHint:SetPoint("BOTTOMLEFT", channelSection, "BOTTOMLEFT", 14, 8) + self.cfgChannelHint:SetTextColor(0.84, 0.8, 0.86) + self.cfgChannelHint:SetText("已加入频道:") + + local maxChannelChecks = 15 + for i = 1, maxChannelChecks do + local slot = i + local col = math.mod(i - 1, 3) + local row = math.floor((i - 1) / 3) + local x = 14 + col * 165 + local y = -24 - row * 24 + + local cb = CreateFrame("CheckButton", NextConfigWidget("ChannelCheck"), channelSection, "UICheckButtonTemplate") + cb:SetWidth(20) + cb:SetHeight(20) + cb:SetPoint("TOPLEFT", channelSection, "TOPLEFT", x, y) + StyleCfgCheck(cb) + + local label = _G[cb:GetName() .. "Text"] + if label then + label:SetText("") + label:SetWidth(140) + label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + end + + cb:SetScript("OnClick", function() + local name = this.channelName + if not name or name == "" then return end + SFrames.Chat:SetActiveTabChannelFilter(name, this:GetChecked() and true or false) + SFrames.Chat:ApplyConfig() + SFrames.Chat:RefreshConfigFrame() + end) + + cb.Refresh = function() + local channels = SFrames.Chat:GetJoinedChannels() + local info = channels[slot] + if info then + cb.channelName = info.name + if label then label:SetText(ShortText(info.name, 24)) end + cb:SetChecked(SFrames.Chat:GetTabChannelFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false) + cb:Show() + else + cb.channelName = nil + cb:SetChecked(false) + if label then label:SetText("") end + cb:Hide() + end + end + + AddControl(cb) + table.insert(self.cfgChannelChecks, cb) + end + + + local okBtn = CreateCfgButton(panel, "保存", 115, -730, 140, 26, function() + SFrames.Chat.configFrame:Hide() + end) + StyleCfgButton(okBtn) + AddBtnIcon(okBtn, "save") + + local reloadBtn = CreateCfgButton(panel, "保存并重载", 285, -730, 140, 26, function() + ReloadUI() + end) + StyleCfgButton(reloadBtn) + AddBtnIcon(reloadBtn, "save") + + panel.controls = controls + self.configControls = controls + self.configFrame = panel +end + +function SFrames.Chat:OpenConfigFrame() + self:EnsureConfigFrame() + if not self.configFrame then return end + self:RefreshConfigFrame() + self.configFrame:Show() + self.configFrame:Raise() +end + +function SFrames.Chat:ToggleConfigFrame() + self:EnsureConfigFrame() + if not self.configFrame then return end + if self.configFrame:IsShown() then + self.configFrame:Hide() + else + self:OpenConfigFrame() + end +end + +local CONFIG_PAGE_ORDER = { + { key = "window", label = "窗口", title = "聊天窗口", desc = "尺寸、缩放、边框和输入框位置。", icon = "settings" }, + { key = "tabs", label = "标签", title = "标签管理", desc = "切换、重命名、新建和删除聊天标签。", icon = "chat" }, + { key = "filters", label = "过滤", title = "消息过滤", desc = "为当前标签设置消息类型和频道接收规则。", icon = "settings" }, + { key = "translate", label = "AI翻译", title = "AI 翻译", desc = "为当前标签配置自动翻译范围和频道翻译。", icon = "ai" }, + { key = "hc", label = "硬核设置", title = "硬核生存选项", desc = "全局硬核控制、死亡通报过滤及等级限制。", icon = "skull" }, +} + +local CONFIG_PAGE_MAP = {} +for i = 1, table.getn(CONFIG_PAGE_ORDER) do + local info = CONFIG_PAGE_ORDER[i] + CONFIG_PAGE_MAP[info.key] = info +end + +local function GetConfigPageInfo(pageKey) + return CONFIG_PAGE_MAP[pageKey] or CONFIG_PAGE_MAP.window +end + +local function GetEditBoxPositionText(mode) + if mode == "top" then return "顶部" end + if mode == "free" then return "自由拖动" end + return "底部" +end + +local function SetConfigNavButtonActive(btn, active) + if not btn then return end + local bg = CFG_THEME.buttonBg + local border = CFG_THEME.buttonBorder + local text = CFG_THEME.buttonText + if active then + bg = { 0.32, 0.16, 0.26, 0.98 } + border = CFG_THEME.btnHoverBd or { 0.80, 0.48, 0.64, 0.98 } + text = { 1, 0.92, 0.96 } + end + if btn.SetBackdropColor then + btn:SetBackdropColor(bg[1], bg[2], bg[3], bg[4]) + end + if btn.SetBackdropBorderColor then + btn:SetBackdropBorderColor(border[1], border[2], border[3], border[4]) + end + local fs = btn.GetFontString and btn:GetFontString() + if fs then + fs:SetTextColor(text[1], text[2], text[3]) + end +end + +function SFrames.Chat:RefreshConfigNavButtons() + if not self.configNavButtons then return end + local activePage = self.configActivePage or "window" + for key, btn in pairs(self.configNavButtons) do + SetConfigNavButtonActive(btn, key == activePage) + end +end + +function SFrames.Chat:ShowConfigPage(pageKey) + if not self.configFrame then return end + local info = GetConfigPageInfo(pageKey) + self.configActivePage = info.key + + if self.configPages then + for key, page in pairs(self.configPages) do + if page then + if key == info.key then + page:Show() + else + page:Hide() + end + end + end + end + + if self.configPageTitle then + self.configPageTitle:SetText(info.title or "") + end + if self.configPageDesc then + self.configPageDesc:SetText(info.desc or "") + end + + self:RefreshConfigNavButtons() +end + +function SFrames.Chat:RefreshConfigFrame() + if not self.configFrame then return end + + if not self.configActivePage then + self.configActivePage = "window" + end + + if self.configSidebarTabText then + self.configSidebarTabText:SetText(self:GetConfigFrameActiveTabName()) + end + + local pageInfo = GetConfigPageInfo(self.configActivePage) + if self.configPageTitle then + self.configPageTitle:SetText(pageInfo.title or "") + end + if self.configPageDesc then + self.configPageDesc:SetText(pageInfo.desc or "") + end + + if self.cfgCurrentTabText then + self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName()) + end + if self.cfgFilterTabText then + self.cfgFilterTabText:SetText("过滤目标: " .. self:GetConfigFrameActiveTabName()) + end + if self.cfgTranslateTabText then + self.cfgTranslateTabText:SetText("翻译目标: " .. self:GetConfigFrameActiveTabName()) + end + + if self.cfgChannelHint then + local channels = self:GetJoinedChannels() + self.cfgChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels))) + end + if self.cfgTranslateChannelHint then + local channels = self:GetJoinedChannels() + self.cfgTranslateChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels))) + end + + if self.cfgRenameBox and self.cfgRenameBox.Refresh then + self.cfgRenameBox:Refresh() + end + + if self.cfgDeleteTabButton then + local protected = self:IsTabProtected(self:GetActiveTabIndex()) + if protected and self.cfgDeleteTabButton.Disable then + self.cfgDeleteTabButton:Disable() + elseif (not protected) and self.cfgDeleteTabButton.Enable then + self.cfgDeleteTabButton:Enable() + end + end + + if self.cfgTabProtectedText then + if self:IsTabProtected(self:GetActiveTabIndex()) then + self.cfgTabProtectedText:SetText("当前标签受保护,不能删除。") + self.cfgTabProtectedText:SetTextColor(0.95, 0.72, 0.72) + else + self.cfgTabProtectedText:SetText("当前标签可自由调整过滤和翻译设置。") + self.cfgTabProtectedText:SetTextColor(0.72, 0.88, 0.76) + end + end + + if self.cfgInputModeText then + self.cfgInputModeText:SetText("当前输入框位置: " .. GetEditBoxPositionText(EnsureDB().editBoxPosition)) + end + + if self.cfgWindowSummaryText then + local db = EnsureDB() + self.cfgWindowSummaryText:SetText("当前尺寸: " .. tostring(db.width) .. " x " .. tostring(db.height) .. " 缩放: " .. string.format("%.2f", db.scale) .. " 背景: " .. string.format("%.0f%%", (db.bgAlpha or DEFAULTS.bgAlpha) * 100)) + end + + if self.cfgTranslateStatusText then + if self:CanUseAutoTranslateAPI() then + self.cfgTranslateStatusText:SetText("STranslate API 已就绪,自动翻译可用。") + self.cfgTranslateStatusText:SetTextColor(0.72, 0.9, 0.78) + else + self.cfgTranslateStatusText:SetText("未检测到可用的 STranslate API,开启后也不会发起翻译请求。") + self.cfgTranslateStatusText:SetTextColor(0.95, 0.76, 0.62) + end + end + + if self.configControls then + for i = 1, table.getn(self.configControls) do + local ctrl = self.configControls[i] + if ctrl and ctrl.Refresh then + ctrl:Refresh() + end + end + end + + self:RefreshConfigNavButtons() +end + +function SFrames.Chat:RefreshTranslateConfigFrame() + self:RefreshConfigFrame() +end + +function SFrames.Chat:EnsureTranslateConfigFrame() + self:EnsureConfigFrame() +end + +function SFrames.Chat:OpenTranslateConfigFrame() + self:OpenConfigFrame("translate") +end + +function SFrames.Chat:ToggleTranslateConfigFrame() + self:ToggleConfigFrame("translate") +end + +function SFrames.Chat:EnsureConfigFrame() + if self.configFrame then return end + + local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + local panel = CreateFrame("Frame", "SFramesChatConfigPanel", UIParent) + panel:SetWidth(780) + panel:SetHeight(630) + panel:SetPoint("CENTER", UIParent, "CENTER", 120, 0) + panel:SetMovable(true) + panel:EnableMouse(true) + panel:SetClampedToScreen(true) + panel:SetFrameStrata("DIALOG") + panel:SetFrameLevel(150) + panel:RegisterForDrag("LeftButton") + panel:SetScript("OnDragStart", function() this:StartMoving() end) + panel:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + table.insert(UISpecialFrames, "SFramesChatConfigPanel") + + if SFrames and SFrames.CreateBackdrop then + SFrames:CreateBackdrop(panel) + else + panel:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = true, tileSize = 32, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + end + panel:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], CFG_THEME.panelBg[4]) + panel:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], CFG_THEME.panelBorder[4]) + + local title = panel:CreateFontString(nil, "OVERLAY") + title:SetFont(fontPath, 15, "OUTLINE") + title:SetPoint("TOPLEFT", panel, "TOPLEFT", 18, -14) + title:SetText("Nanami 聊天设置") + title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) + + local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -4, -4) + + local sidebar = CreateCfgSection(panel, "导航", 12, -42, 154, 532, fontPath) + + local sideLabel = sidebar:CreateFontString(nil, "OVERLAY") + sideLabel:SetFont(fontPath, 10, "OUTLINE") + sideLabel:SetPoint("TOPLEFT", sidebar, "TOPLEFT", 12, -32) + sideLabel:SetText("当前标签") + sideLabel:SetTextColor(0.72, 0.72, 0.78) + + self.configSidebarTabText = sidebar:CreateFontString(nil, "OVERLAY") + self.configSidebarTabText:SetFont(fontPath, 14, "OUTLINE") + self.configSidebarTabText:SetPoint("TOPLEFT", sidebar, "TOPLEFT", 12, -50) + self.configSidebarTabText:SetText(self:GetConfigFrameActiveTabName()) + self.configSidebarTabText:SetTextColor(0.96, 0.94, 0.98) + + local sideTip = sidebar:CreateFontString(nil, "OVERLAY") + sideTip:SetFont(fontPath, 10, "OUTLINE") + sideTip:SetPoint("BOTTOMLEFT", sidebar, "BOTTOMLEFT", 12, 14) + sideTip:SetWidth(130) + sideTip:SetJustifyH("LEFT") + sideTip:SetText("把窗口、标签、过滤和翻译拆成独立页面,避免一屏堆满。") + sideTip:SetTextColor(0.72, 0.72, 0.78) + + self.configPageTitle = panel:CreateFontString(nil, "OVERLAY") + self.configPageTitle:SetFont(fontPath, 14, "OUTLINE") + self.configPageTitle:SetPoint("TOPLEFT", panel, "TOPLEFT", 184, -48) + self.configPageTitle:SetText("") + self.configPageTitle:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) + + self.configPageDesc = panel:CreateFontString(nil, "OVERLAY") + self.configPageDesc:SetFont(fontPath, 10, "OUTLINE") + self.configPageDesc:SetPoint("TOPLEFT", panel, "TOPLEFT", 184, -68) + self.configPageDesc:SetText("") + self.configPageDesc:SetTextColor(0.78, 0.78, 0.84) + + self.configNavButtons = {} + local navY = -88 + for i = 1, table.getn(CONFIG_PAGE_ORDER) do + local info = CONFIG_PAGE_ORDER[i] + local btn = CreateCfgButton(sidebar, info.label, 12, navY, 130, 28, function() + SFrames.Chat:ShowConfigPage(info.key) + SFrames.Chat:RefreshConfigFrame() + end) + if info.icon then AddBtnIcon(btn, info.icon, nil, "left") end + btn.pageKey = info.key + local oldLeave = btn:GetScript("OnLeave") + btn:SetScript("OnLeave", function() + if oldLeave then oldLeave() end + if this.pageKey and SFrames and SFrames.Chat and SFrames.Chat.configActivePage == this.pageKey then + SetConfigNavButtonActive(this, true) + end + end) + self.configNavButtons[info.key] = btn + navY = navY - 36 + end + + local content = CreateFrame("Frame", nil, panel) + content:SetPoint("TOPLEFT", panel, "TOPLEFT", 184, -92) + content:SetWidth(584) + content:SetHeight(484) + panel.content = content + + local controls = {} + local function AddControl(ctrl) + table.insert(controls, ctrl) + return ctrl + end + + local pages = {} + local function CreatePage(key) + local page = CreateFrame("Frame", nil, content) + page:SetAllPoints(content) + page:Hide() + pages[key] = page + return page + end + + local windowPage = CreatePage("window") + do + local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 274, fontPath) + AddControl(CreateCfgSlider(appearance, "宽度", 16, -46, 260, 320, 900, 1, + function() return EnsureDB().width end, + function(v) EnsureDB().width = v end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgSlider(appearance, "高度", 308, -46, 260, 120, 460, 1, + function() return EnsureDB().height end, + function(v) EnsureDB().height = v end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgSlider(appearance, "缩放", 16, -108, 260, 0.75, 1.40, 0.05, + function() return EnsureDB().scale end, + function(v) EnsureDB().scale = v end, + function(v) return string.format("%.2f", v) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgSlider(appearance, "字号", 308, -108, 260, 10, 18, 1, + function() return EnsureDB().fontSize end, + function(v) EnsureDB().fontSize = v end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgSlider(appearance, "背景透明度", 16, -170, 260, 0, 1, 0.05, + function() return EnsureDB().bgAlpha end, + function(v) EnsureDB().bgAlpha = v end, + function(v) return string.format("%.0f%%", v * 100) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgCheck(appearance, "启用聊天", 16, -218, + function() return EnsureDB().enable ~= false end, + function(checked) + local oldState = EnsureDB().enable ~= false + if oldState ~= checked then + EnsureDB().enable = (checked == true) + if StaticPopup_Show then + StaticPopup_Show("SFRAMES_CHAT_RELOAD_PROMPT") + else + ReloadUI() + end + end + end, + function() SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgCheck(appearance, "显示边框", 144, -218, + function() return EnsureDB().showBorder ~= false end, + function(checked) EnsureDB().showBorder = (checked == true) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgCheck(appearance, "边框职业色", 288, -218, + function() return EnsureDB().borderClassColor == true end, + function(checked) EnsureDB().borderClassColor = (checked == true) end, + function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end + )) + AddControl(CreateCfgCheck(appearance, "显示等级", 432, -218, + function() return EnsureDB().showPlayerLevel ~= false end, + function(checked) EnsureDB().showPlayerLevel = (checked == true) end, + function() SFrames.Chat:RefreshConfigFrame() end + )) + + self.cfgWindowSummaryText = appearance:CreateFontString(nil, "OVERLAY") + self.cfgWindowSummaryText:SetFont(fontPath, 10, "OUTLINE") + self.cfgWindowSummaryText:SetPoint("BOTTOMLEFT", appearance, "BOTTOMLEFT", 16, 10) + self.cfgWindowSummaryText:SetTextColor(0.74, 0.74, 0.8) + + local inputSection = CreateCfgSection(windowPage, "输入框", 0, -290, 584, 114, fontPath) + self.cfgInputModeText = inputSection:CreateFontString(nil, "OVERLAY") + self.cfgInputModeText:SetFont(fontPath, 11, "OUTLINE") + self.cfgInputModeText:SetPoint("TOPLEFT", inputSection, "TOPLEFT", 16, -30) + self.cfgInputModeText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + self.cfgInputModeText:SetText("") + + CreateCfgButton(inputSection, "底部", 16, -52, 92, 22, function() + EnsureDB().editBoxPosition = "bottom" + SFrames.Chat:StyleEditBox() + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(inputSection, "顶部", 114, -52, 92, 22, function() + EnsureDB().editBoxPosition = "top" + SFrames.Chat:StyleEditBox() + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(inputSection, "自由拖动", 212, -52, 108, 22, function() + EnsureDB().editBoxPosition = "free" + SFrames.Chat:StyleEditBox() + SFrames.Chat:RefreshConfigFrame() + DEFAULT_CHAT_FRAME:AddMessage("|cffffd100Nanami-UI:|r 按住 Alt 可拖动输入框。") + end) + + local inputTip = inputSection:CreateFontString(nil, "OVERLAY") + inputTip:SetFont(fontPath, 10, "OUTLINE") + inputTip:SetPoint("BOTTOMLEFT", inputSection, "BOTTOMLEFT", 16, 10) + inputTip:SetWidth(540) + inputTip:SetJustifyH("LEFT") + inputTip:SetText("建议优先使用顶部或底部模式;自由拖动适合特殊布局。") + inputTip:SetTextColor(0.74, 0.74, 0.8) + + local actionSection = CreateCfgSection(windowPage, "窗口操作", 0, -398, 584, 96, fontPath) + CreateCfgButton(actionSection, "重置位置", 16, -32, 108, 24, function() + SFrames.Chat:ResetPosition() + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(actionSection, "解锁", 132, -32, 108, 24, function() + if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end + end) + CreateCfgButton(actionSection, "锁定", 248, -32, 108, 24, function() + if SFrames and SFrames.LockFrames then SFrames:LockFrames() end + end) + CreateCfgButton(actionSection, "重置私聊图标位置", 364, -32, 160, 24, function() + if SFramesDB then SFramesDB.whisperBtnPos = nil end + if SFrames and SFrames.Chat and SFrames.Chat.frame then + local f = SFrames.Chat.frame + if f.whisperButton and f.configButton then + f.whisperButton:ClearAllPoints() + f.whisperButton:SetPoint("RIGHT", f.configButton, "LEFT", -6, 0) + end + end + SFrames.Chat:RefreshConfigFrame() + end) + end + + local tabsPage = CreatePage("tabs") + do + local tabSection = CreateCfgSection(tabsPage, "当前标签", 0, 0, 584, 118, fontPath) + self.cfgCurrentTabText = tabSection:CreateFontString(nil, "OVERLAY") + self.cfgCurrentTabText:SetFont(fontPath, 12, "OUTLINE") + self.cfgCurrentTabText:SetPoint("TOPLEFT", tabSection, "TOPLEFT", 16, -30) + self.cfgCurrentTabText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + + CreateCfgButton(tabSection, "上一个", 16, -54, 92, 22, function() + SFrames.Chat:StepTab(-1) + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(tabSection, "下一个", 114, -54, 92, 22, function() + SFrames.Chat:StepTab(1) + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(tabSection, "新建", 212, -54, 92, 22, function() + SFrames.Chat:PromptNewTab() + SFrames.Chat:RefreshConfigFrame() + end) + self.cfgDeleteTabButton = CreateCfgButton(tabSection, "删除", 310, -54, 92, 22, function() + SFrames.Chat:DeleteActiveTab() + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(tabSection, "前往过滤", 408, -54, 112, 22, function() + SFrames.Chat:ShowConfigPage("filters") + SFrames.Chat:RefreshConfigFrame() + end) + + local renameSection = CreateCfgSection(tabsPage, "重命名", 0, -134, 584, 84, fontPath) + self.cfgRenameBox = CreateCfgEditBox(renameSection, 16, -38, 250, 20, + function() return SFrames.Chat:GetConfigFrameActiveTabName() end, + function(text) + SFrames.Chat:RenameActiveTab(text) + SFrames.Chat:RefreshConfigFrame() + end + ) + CreateCfgButton(renameSection, "应用名称", 280, -36, 108, 22, function() + SFrames.Chat:RenameActiveTab(SFrames.Chat.cfgRenameBox:GetText() or "") + SFrames.Chat:RefreshConfigFrame() + end) + + local infoSection = CreateCfgSection(tabsPage, "状态与快捷入口", 0, -234, 584, 116, fontPath) + self.cfgTabProtectedText = infoSection:CreateFontString(nil, "OVERLAY") + self.cfgTabProtectedText:SetFont(fontPath, 11, "OUTLINE") + self.cfgTabProtectedText:SetPoint("TOPLEFT", infoSection, "TOPLEFT", 16, -30) + self.cfgTabProtectedText:SetWidth(540) + self.cfgTabProtectedText:SetJustifyH("LEFT") + + CreateCfgButton(infoSection, "消息过滤", 16, -66, 110, 22, function() + SFrames.Chat:ShowConfigPage("filters") + SFrames.Chat:RefreshConfigFrame() + end) + local aiBtn = CreateCfgButton(infoSection, "AI 翻译", 132, -66, 110, 22, function() + SFrames.Chat:ShowConfigPage("translate") + SFrames.Chat:RefreshConfigFrame() + end) + AddBtnIcon(aiBtn, "ai") + + local infoTip = infoSection:CreateFontString(nil, "OVERLAY") + infoTip:SetFont(fontPath, 10, "OUTLINE") + infoTip:SetPoint("BOTTOMLEFT", infoSection, "BOTTOMLEFT", 16, 12) + infoTip:SetWidth(540) + infoTip:SetJustifyH("LEFT") + infoTip:SetText("默认综合/战斗标签可能受保护。自定义标签建议先设置频道过滤,再配置 AI 翻译。") + infoTip:SetTextColor(0.74, 0.74, 0.8) + end + + local filtersPage = CreatePage("filters") + do + local headerSection = CreateCfgSection(filtersPage, "当前过滤目标", 0, 0, 584, 92, fontPath) + self.cfgFilterTabText = headerSection:CreateFontString(nil, "OVERLAY") + self.cfgFilterTabText:SetFont(fontPath, 12, "OUTLINE") + self.cfgFilterTabText:SetPoint("TOPLEFT", headerSection, "TOPLEFT", 16, -30) + self.cfgFilterTabText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + + CreateCfgButton(headerSection, "上一个", 16, -52, 92, 22, function() + SFrames.Chat:StepTab(-1) + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(headerSection, "下一个", 114, -52, 92, 22, function() + SFrames.Chat:StepTab(1) + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(headerSection, "标签管理", 212, -52, 108, 22, function() + SFrames.Chat:ShowConfigPage("tabs") + SFrames.Chat:RefreshConfigFrame() + end) + + local filterSection = CreateCfgSection(filtersPage, "消息与频道接收", 0, -108, 584, 380, fontPath) + + CreateCfgButton(filterSection, "全选", 16, -26, 60, 20, function() + for _, def in ipairs(FILTER_DEFS) do + SFrames.Chat:SetActiveTabFilter(def.key, true) + end + local channels = SFrames.Chat:GetJoinedChannels() + for i = 1, table.getn(channels) do + SFrames.Chat:SetActiveTabChannelFilter(channels[i].name, true) + end + SFrames.Chat:ApplyConfig() + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(filterSection, "取消全选", 84, -26, 75, 20, function() + for _, def in ipairs(FILTER_DEFS) do + SFrames.Chat:SetActiveTabFilter(def.key, false) + end + local channels = SFrames.Chat:GetJoinedChannels() + for i = 1, table.getn(channels) do + SFrames.Chat:SetActiveTabChannelFilter(channels[i].name, false) + end + SFrames.Chat:ApplyConfig() + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(filterSection, "反选", 167, -26, 60, 20, function() + local actIdx = SFrames.Chat:GetActiveTabIndex() + local tab = SFrames.Chat:GetActiveTab() + for _, def in ipairs(FILTER_DEFS) do + local state = tab and tab.filters and tab.filters[def.key] ~= false + SFrames.Chat:SetActiveTabFilter(def.key, not state) + end + local channels = SFrames.Chat:GetJoinedChannels() + for i = 1, table.getn(channels) do + local state = SFrames.Chat:GetTabChannelFilter(actIdx, channels[i].name) + SFrames.Chat:SetActiveTabChannelFilter(channels[i].name, not state) + end + SFrames.Chat:ApplyConfig() + SFrames.Chat:RefreshConfigFrame() + end) + + local nextIndex = 0 + for i = 1, table.getn(FILTER_DEFS) do + local def = FILTER_DEFS[i] + local col = math.mod(nextIndex, 3) + local row = math.floor(nextIndex / 3) + local x = 16 + col * 182 + local y = -60 - row * 24 + + AddControl(CreateCfgCheck(filterSection, def.label, x, y, + function() + local tab = SFrames.Chat:GetActiveTab() + return tab and tab.filters and tab.filters[def.key] ~= false + end, + function(checked) + SFrames.Chat:SetActiveTabFilter(def.key, checked) + end, + function() + SFrames.Chat:ApplyConfig() + SFrames.Chat:RefreshConfigFrame() + end + )) + nextIndex = nextIndex + 1 + end + + -- Channel sub-header: separator, hint, refresh button + local chSepRow = math.ceil(nextIndex / 3) + local chSepY = -60 - chSepRow * 24 + + local chSepLine = filterSection:CreateTexture(nil, "ARTWORK") + chSepLine:SetTexture(1, 1, 1, 0.15) + chSepLine:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, chSepY + 6) + chSepLine:SetWidth(540) + chSepLine:SetHeight(1) + + local chSepLabel = filterSection:CreateFontString(nil, "OVERLAY") + chSepLabel:SetFont(fontPath, 11, "OUTLINE") + chSepLabel:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, chSepY - 4) + chSepLabel:SetText("频道过滤") + chSepLabel:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) + + self.cfgChannelHint = filterSection:CreateFontString(nil, "OVERLAY") + self.cfgChannelHint:SetFont(fontPath, 10, "OUTLINE") + self.cfgChannelHint:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 120, chSepY - 5) + self.cfgChannelHint:SetTextColor(0.84, 0.80, 0.86) + self.cfgChannelHint:SetText("已加入频道:") + + CreateCfgButton(filterSection, "刷新频道列表", 420, chSepY - 2, 100, 18, function() + SFrames.Chat:RefreshConfigFrame() + end) + + nextIndex = (chSepRow + 1) * 3 + + self.cfgChannelChecks = {} + for i = 1, 15 do + local slot = i + local col = math.mod(nextIndex, 3) + local row = math.floor(nextIndex / 3) + local x = 16 + col * 182 + local y = -60 - row * 24 + + local cb = CreateFrame("CheckButton", NextConfigWidget("ChannelCheck"), filterSection, "UICheckButtonTemplate") + cb:SetWidth(20) + cb:SetHeight(20) + cb:SetPoint("TOPLEFT", filterSection, "TOPLEFT", x, y) + StyleCfgCheck(cb) + + local label = _G[cb:GetName() .. "Text"] + if label then + label:SetText("") + label:SetWidth(150) + label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + end + + cb:SetScript("OnClick", function() + local name = this.channelName + if not name or name == "" then return end + SFrames.Chat:SetActiveTabChannelFilter(name, this:GetChecked() and true or false) + SFrames.Chat:ApplyConfig() + SFrames.Chat:RefreshConfigFrame() + end) + + cb.Refresh = function() + local channels = SFrames.Chat:GetJoinedChannels() + local info = channels[slot] + if info then + cb.channelName = info.name + if label then label:SetText(ShortText(info.name, 24)) end + cb:SetChecked(SFrames.Chat:GetTabChannelFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false) + cb:Show() + else + cb.channelName = nil + cb:SetChecked(false) + if label then label:SetText("") end + cb:Hide() + end + end + + AddControl(cb) + table.insert(self.cfgChannelChecks, cb) + nextIndex = nextIndex + 1 + end + end + + local translatePage = CreatePage("translate") + do + local headerSection = CreateCfgSection(translatePage, "当前翻译目标", 0, 0, 584, 104, fontPath) + self.cfgTranslateTabText = headerSection:CreateFontString(nil, "OVERLAY") + self.cfgTranslateTabText:SetFont(fontPath, 12, "OUTLINE") + self.cfgTranslateTabText:SetPoint("TOPLEFT", headerSection, "TOPLEFT", 16, -30) + self.cfgTranslateTabText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + + self.cfgTranslateStatusText = headerSection:CreateFontString(nil, "OVERLAY") + self.cfgTranslateStatusText:SetFont(fontPath, 10, "OUTLINE") + self.cfgTranslateStatusText:SetPoint("TOPLEFT", headerSection, "TOPLEFT", 16, -50) + self.cfgTranslateStatusText:SetWidth(540) + self.cfgTranslateStatusText:SetJustifyH("LEFT") + + CreateCfgButton(headerSection, "上一个", 16, -72, 92, 22, function() + SFrames.Chat:StepTab(-1) + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(headerSection, "下一个", 114, -72, 92, 22, function() + SFrames.Chat:StepTab(1) + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(headerSection, "频道过滤", 212, -72, 108, 22, function() + SFrames.Chat:ShowConfigPage("filters") + SFrames.Chat:RefreshConfigFrame() + end) + + local filterSection = CreateCfgSection(translatePage, "消息与频道翻译", 0, -120, 584, 360, fontPath) + + CreateCfgButton(filterSection, "全选", 16, -26, 60, 20, function() + for _, key in ipairs(TRANSLATE_FILTER_ORDER) do + SFrames.Chat:SetActiveTabTranslateFilter(key, true) + end + local channels = SFrames.Chat:GetJoinedChannels() + for i = 1, table.getn(channels) do + SFrames.Chat:SetActiveTabChannelTranslateFilter(channels[i].name, true) + end + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(filterSection, "取消全选", 84, -26, 75, 20, function() + for _, key in ipairs(TRANSLATE_FILTER_ORDER) do + SFrames.Chat:SetActiveTabTranslateFilter(key, false) + end + local channels = SFrames.Chat:GetJoinedChannels() + for i = 1, table.getn(channels) do + SFrames.Chat:SetActiveTabChannelTranslateFilter(channels[i].name, false) + end + SFrames.Chat:RefreshConfigFrame() + end) + CreateCfgButton(filterSection, "反选", 167, -26, 60, 20, function() + local actIdx = SFrames.Chat:GetActiveTabIndex() + for _, key in ipairs(TRANSLATE_FILTER_ORDER) do + local state = SFrames.Chat:GetTabTranslateFilter(actIdx, key) + SFrames.Chat:SetActiveTabTranslateFilter(key, not state) + end + local channels = SFrames.Chat:GetJoinedChannels() + for i = 1, table.getn(channels) do + local state = SFrames.Chat:GetTabChannelTranslateFilter(actIdx, channels[i].name) + SFrames.Chat:SetActiveTabChannelTranslateFilter(channels[i].name, not state) + end + SFrames.Chat:RefreshConfigFrame() + end) + + local nextIndex = 0 + for i = 1, table.getn(TRANSLATE_FILTER_ORDER) do + local key = TRANSLATE_FILTER_ORDER[i] + local col = math.mod(nextIndex, 3) + local row = math.floor(nextIndex / 3) + local x = 16 + col * 182 + local y = -60 - row * 24 + + AddControl(CreateCfgCheck(filterSection, GetFilterLabel(key), x, y, + function() + return SFrames.Chat:GetTabTranslateFilter(SFrames.Chat:GetActiveTabIndex(), key) + end, + function(checked) + SFrames.Chat:SetActiveTabTranslateFilter(key, checked) + end, + function() + SFrames.Chat:RefreshConfigFrame() + end + )) + nextIndex = nextIndex + 1 + end + + -- Channel sub-header for translate page + local tchSepRow = math.ceil(nextIndex / 3) + local tchSepY = -60 - tchSepRow * 24 + + local tchSepLine = filterSection:CreateTexture(nil, "ARTWORK") + tchSepLine:SetTexture(1, 1, 1, 0.15) + tchSepLine:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, tchSepY + 6) + tchSepLine:SetWidth(540) + tchSepLine:SetHeight(1) + + local tchSepLabel = filterSection:CreateFontString(nil, "OVERLAY") + tchSepLabel:SetFont(fontPath, 11, "OUTLINE") + tchSepLabel:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, tchSepY - 4) + tchSepLabel:SetText("频道翻译") + tchSepLabel:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) + + self.cfgTranslateChannelHint = filterSection:CreateFontString(nil, "OVERLAY") + self.cfgTranslateChannelHint:SetFont(fontPath, 10, "OUTLINE") + self.cfgTranslateChannelHint:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 120, tchSepY - 5) + self.cfgTranslateChannelHint:SetTextColor(0.84, 0.80, 0.86) + self.cfgTranslateChannelHint:SetText("已加入频道:") + + CreateCfgButton(filterSection, "刷新频道列表", 420, tchSepY - 2, 100, 18, function() + SFrames.Chat:RefreshConfigFrame() + end) + + nextIndex = (tchSepRow + 1) * 3 + + self.translateChannelChecks = {} + for i = 1, 15 do + local slot = i + local col = math.mod(nextIndex, 3) + local row = math.floor(nextIndex / 3) + local x = 16 + col * 182 + local y = -60 - row * 24 + + local cb = CreateFrame("CheckButton", NextConfigWidget("TranslateChannelCheck"), filterSection, "UICheckButtonTemplate") + cb:SetWidth(20) + cb:SetHeight(20) + cb:SetPoint("TOPLEFT", filterSection, "TOPLEFT", x, y) + StyleCfgCheck(cb) + + local label = _G[cb:GetName() .. "Text"] + if label then + label:SetText("") + label:SetWidth(150) + label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + end + + cb:SetScript("OnClick", function() + local name = this.channelName + if not name or name == "" then return end + SFrames.Chat:SetActiveTabChannelTranslateFilter(name, this:GetChecked() and true or false) + SFrames.Chat:RefreshConfigFrame() + end) + + cb.Refresh = function() + local channels = SFrames.Chat:GetJoinedChannels() + local info = channels[slot] + if info then + cb.channelName = info.name + if label then + label:SetText(ShortText(info.name, 24)) + label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + end + cb:SetChecked(SFrames.Chat:GetTabChannelTranslateFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false) + cb:Enable() + cb:Show() + else + cb.channelName = nil + cb:SetChecked(false) + if label then label:SetText("") end + cb:Hide() + end + end + + AddControl(cb) + table.insert(self.translateChannelChecks, cb) + nextIndex = nextIndex + 1 + end + end + + local hcPage = CreatePage("hc") + do + local hcControls = CreateCfgSection(hcPage, "硬核生存服务器专属", 0, 0, 584, 182, fontPath) + + AddControl(CreateCfgCheck(hcControls, "全局彻底关闭硬核频道接收", 16, -30, + function() return EnsureDB().hcGlobalDisable == true end, + function(checked) EnsureDB().hcGlobalDisable = (checked == true) end, + function() SFrames.Chat:RefreshConfigFrame() end + )) + + local hcTip = hcControls:CreateFontString(nil, "OVERLAY") + hcTip:SetFont(fontPath, 10, "OUTLINE") + hcTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -56) + hcTip:SetWidth(540) + hcTip:SetJustifyH("LEFT") + hcTip:SetText("彻底无视HC频道的强制聊天推送。勾选后,所有标签都不会收到硬核频道内容。") + hcTip:SetTextColor(0.8, 0.7, 0.7) + + AddControl(CreateCfgCheck(hcControls, "全局屏蔽玩家死亡/满级信息", 16, -86, + function() return EnsureDB().hcDeathDisable == true end, + function(checked) EnsureDB().hcDeathDisable = (checked == true) end, + function() SFrames.Chat:RefreshConfigFrame() end + )) + + local deathTip = hcControls:CreateFontString(nil, "OVERLAY") + deathTip:SetFont(fontPath, 10, "OUTLINE") + deathTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -112) + deathTip:SetWidth(540) + deathTip:SetJustifyH("LEFT") + deathTip:SetText("关闭那些“某某在XX级死亡”的系统提示。") + deathTip:SetTextColor(0.8, 0.7, 0.7) + + AddControl(CreateCfgSlider(hcControls, "最低死亡通报等级", 340, -82, 210, 0, 60, 1, + function() return EnsureDB().hcDeathLevelMin or 0 end, + function(v) EnsureDB().hcDeathLevelMin = v end, + function(v) return (v == 0) and "所有击杀" or (tostring(v) .. " 级及以上") end, + function() SFrames.Chat:RefreshConfigFrame() end + )) + end + + local close = CreateCfgButton(panel, "保存", 430, -588, 150, 28, function() + SFrames.Chat.configFrame:Hide() + local db = EnsureDB() + + -- Send Hardcore specific commands on Save + if SFrames.Chat.initialHcGlobalDisable ~= nil and db.hcGlobalDisable ~= SFrames.Chat.initialHcGlobalDisable then + SendChatMessage(".hcc", "SAY") + SFrames.Chat.initialHcGlobalDisable = db.hcGlobalDisable + end + + if db.hcDeathDisable then + SendChatMessage(".hcm 60", "SAY") + elseif db.hcDeathLevelMin then + SendChatMessage(".hcm " .. tostring(db.hcDeathLevelMin), "SAY") + else + SendChatMessage(".hcm 0", "SAY") + end + end) + StyleCfgButton(close) + AddBtnIcon(close, "save") + + local reload = CreateCfgButton(panel, "保存并重载", 598, -588, 150, 28, function() + ReloadUI() + end) + StyleCfgButton(reload) + AddBtnIcon(reload, "save") + + self.configControls = controls + self.configPages = pages + self.configFrame = panel + self.translateConfigFrame = panel + + -- Auto-refresh channel list while panel is visible + local channelPollTimer = 0 + local lastChannelCount = 0 + panel:SetScript("OnUpdate", function() + channelPollTimer = channelPollTimer + (arg1 or 0) + if channelPollTimer >= 3 then + channelPollTimer = 0 + if not this:IsShown() then return end + local channels = SFrames.Chat:GetJoinedChannels() + local n = table.getn(channels) + if n ~= lastChannelCount then + lastChannelCount = n + SFrames.Chat:RefreshConfigFrame() + end + end + end) + + self:ShowConfigPage(self.configActivePage or "window") +end + +function SFrames.Chat:OpenConfigFrame(pageKey) + self:EnsureConfigFrame() + if not self.configFrame then return end + self.initialHcGlobalDisable = EnsureDB().hcGlobalDisable + self:ShowConfigPage(pageKey or self.configActivePage or "window") + self:RefreshConfigFrame() + self.configFrame:Show() + self.configFrame:Raise() +end + +function SFrames.Chat:ToggleConfigFrame(pageKey) + self:EnsureConfigFrame() + if not self.configFrame then return end + + local targetPage = pageKey or self.configActivePage or "window" + if self.configFrame:IsShown() then + if pageKey and self.configActivePage ~= targetPage then + self:ShowConfigPage(targetPage) + self:RefreshConfigFrame() + self.configFrame:Raise() + else + self.configFrame:Hide() + end + else + self:OpenConfigFrame(targetPage) + end +end + +function SFrames.Chat:HandleSlash(input) + local text = Trim(input) + local _, _, cmd, rest = string.find(text, "^(%S*)%s*(.-)$") + cmd = string.lower(cmd or "") + rest = rest or "" + + if cmd == "" or cmd == "ui" or cmd == "config" or cmd == "panel" then + self:ToggleConfigFrame() + return + end + + if cmd == "help" then + self:PrintHelp() + return + end + + if cmd == "reset" then + self:ResetPosition() + return + end + + if cmd == "size" then + local _, _, w, h = string.find(rest, "^(%d+)%s+(%d+)$") + w = tonumber(w) + h = tonumber(h) + if not w or not h then + if SFrames and SFrames.Print then + SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat size ") + end + return + end + self:SetWindowSize(w, h) + if SFrames and SFrames.Print then + SFrames:Print("闂備胶鍘у畷顒勬晝閵堝桅濠㈣泛顭ù鏍煕閳╁喚娈曟俊娴嬪亾闂? " .. tostring(math.floor(w + 0.5)) .. "x" .. tostring(math.floor(h + 0.5))) + end + return + end + + if cmd == "scale" then + local v = tonumber(rest) + if not v then + if SFrames and SFrames.Print then + SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat scale <0.75-1.4>") + end + return + end + self:SetWindowScale(v) + if SFrames and SFrames.Print then + SFrames:Print("闂備胶鍘у畷顒勬晝閵堝桅濠㈣泛顭ù鏍煕閳╁啰鎳呯紒杈ㄥ哺閺岋繝鍩€? " .. string.format("%.2f", Clamp(v, 0.75, 1.4))) + end + return + end + + if cmd == "font" then + local v = tonumber(rest) + if not v then + if SFrames and SFrames.Print then + SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat font <10-18>") + end + return + end + self:SetFontSize(v) + if SFrames and SFrames.Print then + SFrames:Print("闂備胶鍘у畷顒勬晝閵堝桅濠㈣泛澶囬崑鎾斥槈濞嗗秳娌紓? " .. tostring(math.floor(Clamp(v, 10, 18) + 0.5))) + end + return + end + + if cmd == "tab" then + local _, _, sub, subArgs = string.find(rest, "^(%S*)%s*(.-)$") + sub = string.lower(sub or "") + subArgs = subArgs or "" + + if sub == "new" then + if Trim(subArgs) == "" then + self:PromptNewTab() + else + self:AddTab(subArgs) + end + return + elseif sub == "del" or sub == "delete" then + self:DeleteActiveTab() + return + elseif sub == "next" then + self:StepTab(1) + return + elseif sub == "prev" then + self:StepTab(-1) + return + elseif sub == "rename" then + local ok = self:RenameActiveTab(subArgs) + if (not ok) and SFrames and SFrames.Print then + SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat tab rename ") + end + return + else + local idx = tonumber(sub) + if idx then + self:SetActiveTab(idx) + return + end + end + + if SFrames and SFrames.Print then + SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat tab new|del|next|prev|rename|") + end + return + end + + if cmd == "filters" then + self:PrintFilters() + return + end + + if cmd == "filter" then + local _, _, key, state = string.find(rest, "^(%S+)%s*(%S*)$") + key = string.lower(key or "") + state = string.lower(state or "") + + local matched = nil + for i = 1, table.getn(FILTER_DEFS) do + local def = FILTER_DEFS[i] + if key == def.key then + matched = def.key + break + end + end + + if not matched then + if SFrames and SFrames.Print then + SFrames:Print("闂備礁鎼悧婊勭閻愮儤鍋傞柍鈺佸暞娴溿倝鏌涢妷锝呭濠殿喓鍨归—鍐Χ閸涱垳鏆涢梺閫炲苯澧柛濠佺矙椤㈡岸顢楁担鐟邦€撻柣鐘充航閸斿秹寮?/nui chat filters") + end + return + end + + local enabled = nil + if state == "on" or state == "1" or state == "true" then + enabled = true + elseif state == "off" or state == "0" or state == "false" then + enabled = false + end + + if enabled == nil then + if SFrames and SFrames.Print then + SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat filter on|off") + end + return + end + + self:SetActiveTabFilter(matched, enabled) + self:ApplyConfig() + if SFrames and SFrames.Print then + SFrames:Print("闂佸搫顦弲娑樏洪敃鍌氱?" .. matched .. " = " .. BoolText(enabled)) + end + return + end + + if SFrames and SFrames.Print then + SFrames:Print("闂備礁鎼悧婊勭閻愮儤鍋傞柨鐔哄У閸ゅ倸鈹戦悩鎻掆偓鑸电椤栫偞鐓曟繛鎴炵懃閻忓﹪鏌熼煬鎻掆偓婵囦繆閹绢喖纾兼繝鍨姇濞堝弶绻涙潏鍓хК婵炲娲熷?/nui chat help") + end +end + +function SFrames.Chat:CreateContainer() + if self.frame then return end + + local f = CreateFrame("Frame", "SFramesChatContainer", UIParent) + f:SetWidth(DEFAULTS.width) + f:SetHeight(DEFAULTS.height) + f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 30, 30) + f:SetMovable(true) + f:EnableMouse(true) + f:RegisterForDrag("LeftButton") + f:SetClampedToScreen(true) + f:SetFrameStrata("LOW") + f:SetResizable(true) + if f.SetMinResize then f:SetMinResize(320, 120) end + if f.SetMaxResize then f:SetMaxResize(900, 460) end + + f:SetScript("OnDragStart", function() + if IsAltKeyDown() or (SFrames and SFrames.isUnlocked) then + this:StartMoving() + end + end) + + f:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + if SFrames and SFrames.Chat then + SFrames.Chat:SavePosition() + end + end) + + f:SetScript("OnSizeChanged", function() + if SFrames and SFrames.Chat then + SFrames.Chat:SaveSizeFromFrame() + SFrames.Chat:RefreshChatBounds() + SFrames.Chat:RefreshTabButtons() + end + end) + + f:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.9) + f:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0.95) + local chatShadow = CreateFrame("Frame", nil, f) + chatShadow:SetPoint("TOPLEFT", f, "TOPLEFT", -5, 5) + chatShadow:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 5, -5) + chatShadow:SetFrameLevel(math.max(f:GetFrameLevel() - 1, 0)) + chatShadow:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + chatShadow:SetBackdropColor(0, 0, 0, 0.55) + chatShadow:SetBackdropBorderColor(0, 0, 0, 0.4) + + local topGlow = f:CreateTexture(nil, "BACKGROUND") + topGlow:SetTexture("Interface\\Buttons\\WHITE8X8") + topGlow:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) + topGlow:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) + topGlow:SetHeight(20) + topGlow:SetVertexColor(1, 0.58, 0.82, 0.2) + topGlow:Hide() + f.topGlow = topGlow + + local topLine = f:CreateTexture(nil, "BORDER") + topLine:SetTexture("Interface\\Buttons\\WHITE8X8") + topLine:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -22) + topLine:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -22) + topLine:SetHeight(1) + topLine:SetVertexColor(1, 0.69, 0.88, 0.85) + topLine:Hide() + f.topLine = topLine + + local title = CreateFont(f, 11, "LEFT") + title:SetPoint("TOPLEFT", f, "TOPLEFT", 26, -7) + title:SetText("Nanami") + title:SetTextColor(1, 0.82, 0.93) + f.title = title + local configButton = CreateFrame("Button", nil, f) + configButton:SetWidth(14) + configButton:SetHeight(14) + configButton:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -7) + configButton:SetHitRectInsets(-4, -4, -4, -4) + configButton:SetFrameStrata("HIGH") + configButton:SetFrameLevel(f:GetFrameLevel() + 20) + configButton:RegisterForClicks("LeftButtonUp") + + local cfgIcon = SFrames:CreateIcon(configButton, "settings", 12) + cfgIcon:SetPoint("CENTER", configButton, "CENTER", 0, 0) + configButton.cfgIcon = cfgIcon + + configButton:SetScript("OnClick", function() + if SFrames and SFrames.Chat then + SFrames.Chat:ToggleConfigFrame() + end + end) + configButton:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:ClearLines() + GameTooltip:AddLine("聊天设置", 1, 0.84, 0.94) + GameTooltip:AddLine("打开聊天配置面板", 0.85, 0.85, 0.85) + GameTooltip:Show() + end) + configButton:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + f.configButton = configButton + + local whisperButton = CreateFrame("Button", nil, f) + whisperButton:SetFrameStrata("HIGH") + whisperButton:SetWidth(14) + whisperButton:SetHeight(14) + if SFramesDB and SFramesDB.whisperBtnPos then + local pos = SFramesDB.whisperBtnPos + whisperButton:SetPoint(pos.p, f, pos.rp, pos.x, pos.y) + else + whisperButton:SetPoint("RIGHT", configButton, "LEFT", -6, 0) + end + whisperButton:SetHitRectInsets(-4, -4, -4, -4) + whisperButton:SetFrameLevel(f:GetFrameLevel() + 20) + whisperButton:RegisterForClicks("LeftButtonUp") + whisperButton:RegisterForDrag("LeftButton") + whisperButton:SetMovable(true) + whisperButton:SetScript("OnDragStart", function() + if IsAltKeyDown() then + this:StartMoving() + end + end) + whisperButton:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + if not SFramesDB then SFramesDB = {} end + -- Convert to position relative to parent frame f to ensure correct restore after reload + local absX, absY = this:GetCenter() + local fX, fY = f:GetCenter() + local fW, fH = f:GetWidth(), f:GetHeight() + local relX = absX - fX + local relY = absY - fY + SFramesDB.whisperBtnPos = { p = "CENTER", rp = "CENTER", x = relX, y = relY } + end) + + if SFrames and SFrames.CreateBackdrop then + SFrames:CreateBackdrop(whisperButton) + elseif whisperButton.SetBackdrop then + whisperButton:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + end + if whisperButton.SetBackdropColor then + whisperButton:SetBackdropColor(0.1, 0.08, 0.12, 0.9) + end + if whisperButton.SetBackdropBorderColor then + whisperButton:SetBackdropBorderColor(0.5, 0.8, 1.0, 0.92) + end + + local whisperIcon = SFrames:CreateIcon(whisperButton, "chat", 12) + whisperIcon:SetDrawLayer("ARTWORK") + whisperIcon:SetPoint("CENTER", whisperButton, "CENTER", 0, 0) + whisperIcon:SetVertexColor(0.6, 0.85, 1, 0.95) + + local flashFrame = CreateFrame("Frame", nil, whisperButton) + flashFrame:SetAllPoints(whisperButton) + flashFrame:SetFrameLevel(whisperButton:GetFrameLevel() + 1) + local flashTex = flashFrame:CreateTexture(nil, "OVERLAY") + flashTex:SetTexture("Interface\\Buttons\\WHITE8X8") + flashTex:SetAllPoints(flashFrame) + flashTex:SetVertexColor(1, 0.8, 0.2, 0.4) + flashTex:SetBlendMode("ADD") + flashFrame.tex = flashTex + flashFrame:Hide() + whisperButton.flashFrame = flashFrame + + flashFrame:SetScript("OnUpdate", function() + if not this:IsShown() then return end + this.elapsed = (this.elapsed or 0) + arg1 + local alpha = math.abs(math.sin(this.elapsed * 4)) * 0.5 + 0.1 + this.tex:SetAlpha(alpha) + end) + + whisperButton:SetScript("OnClick", function() + if SFrames and SFrames.Whisper then + SFrames.Whisper:Toggle() + end + this.hasUnread = false + if this.flashFrame then this.flashFrame:Hide() end + end) + whisperButton:SetScript("OnEnter", function() + if this.SetBackdropColor then this:SetBackdropColor(0.16, 0.12, 0.2, 0.96) end + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:ClearLines() + GameTooltip:AddLine("私聊对话管理器", 0.6, 0.8, 1) + if this.hasUnread then + GameTooltip:AddLine("你有未读的私聊消息!", 1, 0.8, 0.2) + end + GameTooltip:AddLine("Alt + 拖动 可移动图标", 0.6, 0.6, 0.6) + GameTooltip:Show() + end) + whisperButton:SetScript("OnLeave", function() + if this.SetBackdropColor then this:SetBackdropColor(0.1, 0.08, 0.12, 0.9) end + GameTooltip:Hide() + end) + + f.whisperButton = whisperButton + + local hint = CreateFont(f, 10, "RIGHT") + hint:SetPoint("RIGHT", whisperButton, "LEFT", -8, 0) + hint:SetText("") + hint:SetTextColor(0.86, 0.78, 0.85) + hint:Hide() + f.hint = hint + + local leftCat = SFrames:CreateIcon(f, "logo", 14) + leftCat:SetDrawLayer("OVERLAY") + leftCat:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -5) + leftCat:SetVertexColor(1, 0.82, 0.9, 0.8) + f.leftCat = leftCat + + local watermark = SFrames:CreateIcon(f, "logo", 62) + watermark:SetDrawLayer("BACKGROUND") + watermark:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -8, 8) + watermark:SetVertexColor(1, 0.78, 0.9, 0.08) + f.watermark = watermark + + local shade = f:CreateTexture(nil, "BACKGROUND") + shade:SetTexture("Interface\\Buttons\\WHITE8X8") + shade:SetVertexColor(0, 0, 0, 0.2) + shade:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -30) + shade:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -8, 8) + f.innerShade = shade + + local tabBar = CreateFrame("Frame", nil, f) + tabBar:SetPoint("LEFT", title, "RIGHT", 10, -1) + tabBar:SetPoint("RIGHT", configButton, "LEFT", -8, -1) + tabBar:SetHeight(18) + f.tabBar = tabBar + + local inner = CreateFrame("Frame", nil, f) + inner:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -30) + inner:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -20, 8) + f.inner = inner + + local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + + local scrollUpBtn = CreateFrame("Button", nil, f) + scrollUpBtn:SetWidth(16) + scrollUpBtn:SetHeight(16) + scrollUpBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -30) + scrollUpBtn:SetFrameStrata("HIGH") + local upTxt = scrollUpBtn:CreateFontString(nil, "OVERLAY") + upTxt:SetFont(fontPath, 11, "OUTLINE") + upTxt:SetPoint("CENTER", scrollUpBtn, "CENTER", 1, 1) + upTxt:SetText("▲") + upTxt:SetTextColor(0.5, 0.55, 0.6) + scrollUpBtn:SetScript("OnEnter", function() upTxt:SetTextColor(0.9, 0.85, 1) end) + scrollUpBtn:SetScript("OnLeave", function() upTxt:SetTextColor(0.5, 0.55, 0.6) end) + scrollUpBtn:SetScript("OnClick", function() + if SFrames.Chat.chatFrame then + SFrames.Chat.chatFrame:ScrollToTop() + end + end) + + local scrollDownBtn = CreateFrame("Button", nil, f) + scrollDownBtn:SetWidth(16) + scrollDownBtn:SetHeight(16) + scrollDownBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 20) + scrollDownBtn:SetFrameStrata("HIGH") + local dnTxt = scrollDownBtn:CreateFontString(nil, "OVERLAY") + dnTxt:SetFont(fontPath, 11, "OUTLINE") + dnTxt:SetPoint("CENTER", scrollDownBtn, "CENTER", 1, -1) + dnTxt:SetText("▼") + dnTxt:SetTextColor(0.5, 0.55, 0.6) + scrollDownBtn:SetScript("OnEnter", function() dnTxt:SetTextColor(0.9, 0.85, 1) end) + scrollDownBtn:SetScript("OnLeave", function() dnTxt:SetTextColor(0.5, 0.55, 0.6) end) + scrollDownBtn:SetScript("OnClick", function() + if SFrames.Chat.chatFrame then + SFrames.Chat.chatFrame:ScrollToBottom() + end + end) + + local scrollTrack = f:CreateTexture(nil, "BACKGROUND") + scrollTrack:SetTexture("Interface\\Buttons\\WHITE8X8") + scrollTrack:SetPoint("TOP", scrollUpBtn, "BOTTOM", 0, -2) + scrollTrack:SetPoint("BOTTOM", scrollDownBtn, "TOP", 0, 2) + scrollTrack:SetWidth(4) + scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0.9) + + local resize = CreateFrame("Button", nil, f) + resize:SetWidth(16) + resize:SetHeight(16) + resize:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) + resize:RegisterForDrag("LeftButton") + resize:EnableMouse(true) + resize:SetFrameStrata("HIGH") + + local resizeTex = resize:CreateTexture(nil, "ARTWORK") + resizeTex:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up") + resizeTex:SetAllPoints(resize) + resizeTex:SetVertexColor(1, 0.74, 0.88, 0.92) + + resize:SetScript("OnEnter", function() + resizeTex:SetVertexColor(1, 0.86, 0.94, 1) + end) + resize:SetScript("OnLeave", function() + resizeTex:SetVertexColor(1, 0.74, 0.88, 0.92) + end) + resize:SetScript("OnMouseDown", function() + if not (IsAltKeyDown() or (SFrames and SFrames.isUnlocked)) then return end + f:StartSizing("BOTTOMRIGHT") + end) + resize:SetScript("OnMouseUp", function() + f:StopMovingOrSizing() + if SFrames and SFrames.Chat then + SFrames.Chat:SaveSizeFromFrame() + SFrames.Chat:ApplyConfig() + end + end) + + f.resizeHandle = resize + self.frame = f + + if not self.hiddenConfigButton then + local hiddenConfigButton = CreateFrame("Button", "SFramesChatHiddenConfigButton", UIParent, "UIPanelButtonTemplate") + hiddenConfigButton:SetWidth(74) + hiddenConfigButton:SetHeight(22) + hiddenConfigButton:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 14, 132) + hiddenConfigButton:SetFrameStrata("DIALOG") + hiddenConfigButton:SetFrameLevel(220) + hiddenConfigButton:SetText("Chat Set") + hiddenConfigButton:SetScript("OnClick", function() + if SFrames and SFrames.Chat then + SFrames.Chat:ToggleConfigFrame() + end + end) + hiddenConfigButton:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_TOPLEFT") + GameTooltip:ClearLines() + GameTooltip:AddLine("Chat Settings", 1, 0.84, 0.94) + GameTooltip:AddLine("Shown while chat UI is hidden.", 0.86, 0.86, 0.86) + GameTooltip:AddLine("Click to open Nanami chat config.", 0.86, 0.86, 0.86) + GameTooltip:Show() + end) + hiddenConfigButton:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + StyleCfgButton(hiddenConfigButton) + hiddenConfigButton:Hide() + self.hiddenConfigButton = hiddenConfigButton + end + + local db = EnsureDB() + local saved = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ChatFrame"] + if saved then + f:ClearAllPoints() + f:SetPoint(saved.point, UIParent, saved.relativePoint, saved.xOfs, saved.yOfs) + else + f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 30, 30) + end + f:SetWidth(Clamp(db.width, 320, 900)) + f:SetHeight(Clamp(db.height, 120, 460)) + + -- Background alpha: always show at configured bgAlpha + local bgA = Clamp(db.bgAlpha or DEFAULTS.bgAlpha, 0, 1) + f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA) +end + +function SFrames.Chat:HideDefaultChrome() + for _, objectName in ipairs(HIDDEN_OBJECTS) do + ForceHide(_G[objectName]) + end +end + +function SFrames.Chat:HideTabChrome() + local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7 + + for i = 1, maxWindows do + local prefix = "ChatFrame" .. i + local elements = { + prefix .. "Tab", + prefix .. "ButtonFrame", + prefix .. "UpButton", + prefix .. "DownButton", + prefix .. "BottomButton", + prefix .. "TabLeft", + prefix .. "TabMiddle", + prefix .. "TabRight", + prefix .. "TabSelectedLeft", + prefix .. "TabSelectedMiddle", + prefix .. "TabSelectedRight", + prefix .. "TabHighlightLeft", + prefix .. "TabHighlightMiddle", + prefix .. "TabHighlightRight", + prefix .. "TabFlash", + prefix .. "TabGlow", + } + + for _, objectName in ipairs(elements) do + ForceHide(_G[objectName]) + end + + local text = _G[prefix .. "TabText"] + if text then + ForceInvisible(text) + text.Show = Dummy + end + end + + local dock = _G["GeneralDockManager"] + if dock then + if dock.SetAlpha then dock:SetAlpha(0) end + if dock.EnableMouse then dock:EnableMouse(false) end + end +end + +function SFrames.Chat:IsManagedChatWindow(chatFrame) + if not chatFrame then return false end + local name = chatFrame.GetName and chatFrame:GetName() + if type(name) ~= "string" then return false end + if not string.find(name, "^ChatFrame%d+$") then return false end + if self.frame and self.frame.inner and chatFrame.GetParent and chatFrame:GetParent() == self.frame.inner then + return true + end + if self.GetTabIndexForChatFrame and self:GetTabIndexForChatFrame(chatFrame) then + return true + end + return false +end + +function SFrames.Chat:HookWindowDragBlocker() + if self.dragBlockerHooked then return end + + local function ResolveObjectToChatFrame(object) + local probe = object + local steps = 0 + while probe and steps < 10 do + if probe.chatFrame then + probe = probe.chatFrame + end + + if probe and probe.GetName then + local name = probe:GetName() + if type(name) == "string" then + local _, _, idx = string.find(name, "^ChatFrame(%d+)Tab$") + if idx then + local cf = _G["ChatFrame" .. idx] + if cf then + return cf + end + end + if string.find(name, "^ChatFrame%d+$") then + return probe + end + end + end + + if not (probe and probe.GetParent) then + break + end + probe = probe:GetParent() + steps = steps + 1 + end + return nil + end + + local function ResolveTarget(frame) + local direct = ResolveObjectToChatFrame(frame) + if direct then return direct end + return ResolveObjectToChatFrame(this) + end + + local function Wrap(name) + local orig = _G[name] + if type(orig) ~= "function" then return false end + _G[name] = function(frame, a, b, c, d, e) + local target = ResolveTarget(frame) + if SFrames and SFrames.Chat and SFrames.Chat.IsManagedChatWindow and SFrames.Chat:IsManagedChatWindow(target) then + if target and target.StopMovingOrSizing then + target:StopMovingOrSizing() + end + return + end + return orig(frame, a, b, c, d, e) + end + return true + end + + local hookedAny = false + if Wrap("FCF_StartWindowDrag") then hookedAny = true end + if Wrap("FCF_StartMoving") then hookedAny = true end + if Wrap("FCF_StartDrag") then hookedAny = true end + if Wrap("FloatingChatFrame_OnMouseDown") then hookedAny = true end + + if hookedAny then + self.dragBlockerHooked = true + end +end + +function SFrames.Chat:EnforceChatWindowLock(chatFrame) + if not chatFrame then return end + if FCF_SetLocked then + pcall(function() FCF_SetLocked(chatFrame, 1) end) + end + if FCF_SetWindowLocked then + pcall(function() FCF_SetWindowLocked(chatFrame, 1) end) + end + if chatFrame.SetMovable then + chatFrame:SetMovable(false) + end + if not chatFrame.sfRegisterForDragBlocked and chatFrame.RegisterForDrag then + pcall(function() chatFrame:RegisterForDrag("Button4") end) + chatFrame.sfRegisterForDragBlocked = true + chatFrame.sfOriginalRegisterForDrag = chatFrame.RegisterForDrag + chatFrame.RegisterForDrag = function() end + end + if not chatFrame.sfStartMovingBlocked and chatFrame.StartMoving then + chatFrame.sfStartMovingBlocked = true + chatFrame.StartMoving = function(frame) + if frame and frame.StopMovingOrSizing then + frame:StopMovingOrSizing() + end + end + end + if not chatFrame.sfStartSizingBlocked and chatFrame.StartSizing then + chatFrame.sfStartSizingBlocked = true + chatFrame.StartSizing = function(frame) + if frame and frame.StopMovingOrSizing then + frame:StopMovingOrSizing() + end + end + end + if chatFrame.SetScript then + chatFrame:SetScript("OnMouseDown", function() + if this and this.StopMovingOrSizing then + this:StopMovingOrSizing() + end + if (IsAltKeyDown() or (SFrames and SFrames.isUnlocked)) and SFrames.Chat and SFrames.Chat.frame then + SFrames.Chat.frame:StartMoving() + SFrames.Chat._contentDragging = true + end + end) + chatFrame:SetScript("OnMouseUp", function() + if SFrames.Chat and SFrames.Chat._contentDragging and SFrames.Chat.frame then + SFrames.Chat.frame:StopMovingOrSizing() + SFrames.Chat:SavePosition() + SFrames.Chat._contentDragging = nil + end + if this and this.StopMovingOrSizing then + this:StopMovingOrSizing() + end + end) + chatFrame:SetScript("OnDragStart", function() + if this and this.StopMovingOrSizing then + this:StopMovingOrSizing() + end + end) + chatFrame:SetScript("OnDragStop", function() + if SFrames.Chat and SFrames.Chat._contentDragging and SFrames.Chat.frame then + SFrames.Chat.frame:StopMovingOrSizing() + SFrames.Chat:SavePosition() + SFrames.Chat._contentDragging = nil + end + if this and this.StopMovingOrSizing then + this:StopMovingOrSizing() + end + end) + end + if chatFrame.SetUserPlaced then + chatFrame:SetUserPlaced(false) + end + if chatFrame.StopMovingOrSizing then + chatFrame:StopMovingOrSizing() + end +end + +function SFrames.Chat:StartPositionEnforcer() + if self._positionEnforcerRunning then return end + self._positionEnforcerRunning = true + if not self._positionEnforcer then + self._positionEnforcer = CreateFrame("Frame") + end + local enforcer = self._positionEnforcer + enforcer.elapsed = 0 + enforcer:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + arg1 + if this.elapsed < 0.3 then return end + this.elapsed = 0 + if not (SFrames and SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame.inner and SFrames.Chat.frame:IsShown()) then + return + end + local inner = SFrames.Chat.frame.inner + local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7 + for i = 1, maxWindows do + local cf = _G["ChatFrame" .. tostring(i)] + if cf and SFrames.Chat:IsManagedChatWindow(cf) then + local needsFix = false + if cf.GetParent and cf:GetParent() ~= inner then + cf:SetParent(inner) + needsFix = true + end + if cf.GetPoint then + local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1) + if point ~= "TOPLEFT" or relativeTo ~= inner or relativePoint ~= "TOPLEFT" or (xOfs and xOfs ~= 0) or (yOfs and yOfs ~= 0) then + needsFix = true + end + if cf.GetNumPoints and cf:GetNumPoints() ~= 1 then + needsFix = true + end + end + if needsFix then + cf:ClearAllPoints() + cf:SetPoint("TOPLEFT", inner, "TOPLEFT", 0, 0) + SFrames.Chat:EnforceChatWindowLock(cf) + SFrames.Chat:RefreshChatBounds() + end + end + end + end) +end + +function SFrames.Chat:RefreshChatBounds() + if not (self.chatFrame and self.frame and self.frame.inner) then return end + + -- In WoW 1.12, GetWidth/GetHeight can return 0 for frames sized entirely by anchors. + -- ChatFrame internal scrolling breaks if its explicit size is 0, causing it to render + -- only 1 line at the bottom. We manually calculate the intended size. + local cfg = self:GetConfig() + local parentWidth = self.frame:GetWidth() or cfg.width or 400 + local parentHeight = self.frame:GetHeight() or cfg.height or 200 + + local width = parentWidth - (cfg.sidePadding * 2) + local height = parentHeight - cfg.topPadding - cfg.bottomPadding + + if width < 80 or height < 10 then return end + + self.chatFrame:SetWidth(width + 1) + self.chatFrame:SetWidth(width) + self.chatFrame:SetHeight(height + 1) + self.chatFrame:SetHeight(height) + if self.chatFrame.UpdateScrollRegion then + pcall(function() self.chatFrame:UpdateScrollRegion() end) + end +end + +function SFrames.Chat:StartStabilizer() + if not self.stabilizer then + self.stabilizer = CreateFrame("Frame") + end + + local stabilize = self.stabilizer + stabilize.elapsed = 0 + stabilize.total = 0 + + stabilize:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + arg1 + this.total = this.total + arg1 + + if this.elapsed >= 0.12 then + this.elapsed = 0 + if SFrames and SFrames.Chat then + SFrames.Chat:HideDefaultChrome() + SFrames.Chat:HideTabChrome() + SFrames.Chat:RefreshTabButtons() + -- Only refresh bounds if anchors actually drifted (avoids +1/-1 flicker) + local chat = SFrames.Chat + if chat.chatFrame and chat.frame and chat.frame.inner then + local inner = chat.frame.inner + local point, relativeTo = chat.chatFrame:GetPoint(1) + if point ~= "TOPLEFT" or relativeTo ~= inner then + chat:ReanchorChatFrames() + chat:RefreshChatBounds() + end + end + end + end + + if this.total >= 2 then + this:SetScript("OnUpdate", nil) + end + end) +end + +function SFrames.Chat:EnsureChromeWatcher() + if not self.chromeWatcher then + self.chromeWatcher = CreateFrame("Frame") + end + + local watcher = self.chromeWatcher + watcher.elapsed = 0 + watcher.remaining = math.max(watcher.remaining or 0, 2.5) + if watcher.sfRunning then + return + end + + watcher.sfRunning = true + watcher:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + arg1 + this.remaining = (this.remaining or 0) - arg1 + + if this.elapsed < 0.25 then + if this.remaining <= 0 then + this.sfRunning = false + this:SetScript("OnUpdate", nil) + end + return + end + this.elapsed = 0 + + if not (SFrames and SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame:IsShown()) then + if this.remaining <= 0 then + this.sfRunning = false + this:SetScript("OnUpdate", nil) + end + return + end + + local didReanchor = false + SFrames.Chat:HideDefaultChrome() + SFrames.Chat:HideTabChrome() + local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7 + for i = 1, maxWindows do + local cf = _G["ChatFrame" .. tostring(i)] + if cf then + SFrames.Chat:EnforceChatWindowLock(cf) + if SFrames.Chat:IsManagedChatWindow(cf) and SFrames.Chat.frame and SFrames.Chat.frame.inner then + if cf.GetParent and cf:GetParent() ~= SFrames.Chat.frame.inner then + cf:SetParent(SFrames.Chat.frame.inner) + didReanchor = true + end + if cf.GetPoint then + local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1) + local badAnchor = (point ~= "TOPLEFT" or relativeTo ~= SFrames.Chat.frame.inner or relativePoint ~= "TOPLEFT" or xOfs ~= 0 or yOfs ~= 0) + if badAnchor or (cf.GetNumPoints and cf:GetNumPoints() ~= 2) then + cf:ClearAllPoints() + cf:SetPoint("TOPLEFT", SFrames.Chat.frame.inner, "TOPLEFT", 0, 0) + cf:SetPoint("BOTTOMRIGHT", SFrames.Chat.frame.inner, "BOTTOMRIGHT", 0, 0) + didReanchor = true + end + end + end + end + end + if SFrames.Chat.chatFrame and SFrames.Chat.frame and SFrames.Chat.frame.inner then + local cf = SFrames.Chat.chatFrame + if cf.GetParent and cf:GetParent() ~= SFrames.Chat.frame.inner then + cf:SetParent(SFrames.Chat.frame.inner) + didReanchor = true + end + if cf.GetPoint then + local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1) + local badAnchor = (point ~= "TOPLEFT" or relativeTo ~= SFrames.Chat.frame.inner or relativePoint ~= "TOPLEFT" or xOfs ~= 0 or yOfs ~= 0) + if badAnchor or (cf.GetNumPoints and cf:GetNumPoints() ~= 2) then + cf:ClearAllPoints() + cf:SetPoint("TOPLEFT", SFrames.Chat.frame.inner, "TOPLEFT", 0, 0) + cf:SetPoint("BOTTOMRIGHT", SFrames.Chat.frame.inner, "BOTTOMRIGHT", 0, 0) + didReanchor = true + end + end + end + + if didReanchor then + SFrames.Chat:RefreshChatBounds() + end + + if this.remaining <= 0 then + this.sfRunning = false + this:SetScript("OnUpdate", nil) + end + end) +end + +function SFrames.Chat:ReanchorChatFrames() + if not (self.frame and self.frame.inner) then return end + local inner = self.frame.inner + local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7 + for i = 1, maxWindows do + local cf = _G["ChatFrame" .. tostring(i)] + if cf and self:IsManagedChatWindow(cf) then + if cf.GetParent and cf:GetParent() ~= inner then + cf:SetParent(inner) + end + local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1) + if point ~= "TOPLEFT" or relativeTo ~= inner or relativePoint ~= "TOPLEFT" or xOfs ~= 0 or yOfs ~= 0 then + cf:ClearAllPoints() + cf:SetPoint("TOPLEFT", inner, "TOPLEFT", 0, 0) + cf:SetPoint("BOTTOMRIGHT", inner, "BOTTOMRIGHT", 0, 0) + end + end + end + self:HideDefaultChrome() + self:HideTabChrome() +end + +function SFrames.Chat:ApplyChatFrameBaseStyle(chatFrame, isCombat) + if not chatFrame then return end + if chatFrame.SetFrameStrata then + chatFrame:SetFrameStrata("MEDIUM") + end + if chatFrame.SetFrameLevel and self.frame and self.frame.inner then + chatFrame:SetFrameLevel((self.frame.inner:GetFrameLevel() or self.frame:GetFrameLevel() or 1) + 6) + end + if chatFrame.SetAlpha then chatFrame:SetAlpha(1) end + chatFrame:SetFading(false) + if chatFrame.SetTimeVisible then + chatFrame:SetTimeVisible(999999) + end + if chatFrame.EnableMouseWheel then + chatFrame:EnableMouseWheel(true) + if not chatFrame.sfScrollHooked then + chatFrame.sfScrollHooked = true + chatFrame:SetScript("OnMouseWheel", function() + if arg1 > 0 then + if IsShiftKeyDown() then + this:ScrollToTop() + else + this:ScrollUp() + this:ScrollUp() + this:ScrollUp() + end + elseif arg1 < 0 then + if IsShiftKeyDown() then + this:ScrollToBottom() + else + this:ScrollDown() + this:ScrollDown() + this:ScrollDown() + end + end + end) + end + end + chatFrame:SetJustifyH("LEFT") + if chatFrame.SetSpacing then chatFrame:SetSpacing(1) end + if chatFrame.SetMaxLines then + chatFrame:SetMaxLines((isCombat and 4096) or 1024) + end + if chatFrame.SetHyperlinksEnabled then chatFrame:SetHyperlinksEnabled(1) end + if chatFrame.SetIndentedWordWrap then chatFrame:SetIndentedWordWrap(false) end + if chatFrame.SetShadowOffset then chatFrame:SetShadowOffset(1, -1) end + if chatFrame.SetShadowColor then chatFrame:SetShadowColor(0, 0, 0, 0.92) end + + self:EnforceChatWindowLock(chatFrame) + if not chatFrame.sfDragLockHooked and chatFrame.HookScript then + chatFrame.sfDragLockHooked = true + chatFrame:HookScript("OnDragStart", function() + chatFrame:StopMovingOrSizing() + end) + chatFrame:HookScript("OnDragStop", function() + chatFrame:StopMovingOrSizing() + end) + end + + -- Force disable fading completely + chatFrame:SetFading(false) + if chatFrame.SetTimeVisible then + chatFrame:SetTimeVisible(999999) + end + if not chatFrame.sfFadingHooked then + chatFrame.sfFadingHooked = true + chatFrame.SetFading = function() end + chatFrame.SetTimeVisible = function() end + end +end + +function SFrames.Chat:GetChatFrameForTab(tab) + if not tab then return ChatFrame1, false end + if tab.kind == "combat" and ChatFrame2 then + return ChatFrame2, true + end + + local index = 1 + local db = EnsureDB() + for i=1, table.getn(db.tabs) do + if db.tabs[i] == tab then + index = i + break + end + end + + local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7 + if index > maxWindows then index = maxWindows end + + local cfName = "ChatFrame" .. tostring(index) + local cf = _G[cfName] + + if not cf then + return ChatFrame1, false + end + + return cf, false +end + +function SFrames.Chat:EnsureCombatLogFrame() + if not ChatFrame2 then return end + + if (not self.combatLogAddonLoaded) and LoadAddOn and IsAddOnLoaded then + if not IsAddOnLoaded("Blizzard_CombatLog") then + local loadable = false + if GetAddOnInfo then + local name, _, _, enabled, lod, reason = GetAddOnInfo("Blizzard_CombatLog") + loadable = name and name ~= "" and reason ~= "MISSING" and reason ~= "DISABLED" + end + if loadable then + local ok = pcall(LoadAddOn, "Blizzard_CombatLog") + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff9900Nanami-UI:|r Blizzard_CombatLog not available, skipping.") + end + end + end + self.combatLogAddonLoaded = true + end + + if FCF_SetCombatLog then + pcall(function() + FCF_SetCombatLog(ChatFrame2) + end) + end + + if FCF_SetWindowName then + pcall(function() + FCF_SetWindowName(ChatFrame2, DEFAULT_COMBAT_TAB_NAME) + end) + end + + if ChatFrame2.SetFading then ChatFrame2:SetFading(false) end + if ChatFrame2.SetMaxLines then ChatFrame2:SetMaxLines(4096) end + self.combatLogPrepared = true +end + +function SFrames.Chat:SetupChatFrameForTab(index) + local tab = self:GetTab(index) + if not tab then return end + if not (self.frame and self.frame.inner) then return end + + local chatFrame, isCombat = self:GetChatFrameForTab(tab) + if not chatFrame then return end + if isCombat then + self:EnsureCombatLogFrame() + end + + if not self.chatUndockedFrames then + self.chatUndockedFrames = {} + end + if not self.chatFrameToTabIndex then + self.chatFrameToTabIndex = {} + end + local frameID = chatFrame.GetID and chatFrame:GetID() or 1 + if not self.chatUndockedFrames[frameID] and FCF_UnDockFrame then + pcall(function() FCF_UnDockFrame(chatFrame) end) + self.chatUndockedFrames[frameID] = true + end + self.chatFrameToTabIndex[chatFrame] = index + + chatFrame:ClearAllPoints() + chatFrame:SetParent(self.frame.inner) + chatFrame:SetPoint("TOPLEFT", self.frame.inner, "TOPLEFT", 0, 0) + + local cfg = self:GetConfig() + local width = (self.frame:GetWidth() or cfg.width or 400) - (cfg.sidePadding * 2) + local height = (self.frame:GetHeight() or cfg.height or 180) - cfg.topPadding - cfg.bottomPadding + if width < 80 then width = 80 end + if height < 10 then height = 10 end + chatFrame:SetWidth(width) + chatFrame:SetHeight(height) + + self:ApplyChatFrameBaseStyle(chatFrame, isCombat) + if chatFrame.SetUserPlaced then chatFrame:SetUserPlaced(false) end + if chatFrame.SetMaxLines then chatFrame:SetMaxLines((isCombat and 4096) or 1024) end + if chatFrame.UpdateScrollRegion then pcall(function() chatFrame:UpdateScrollRegion() end) end +end + +function SFrames.Chat:SwitchActiveChatFrame(tab) + if not (self.frame and self.frame.inner) then return end + + local activeChatFrame, isCombat = self:GetChatFrameForTab(tab) + if not activeChatFrame then return end + + local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7 + for i = 1, maxWindows do + local cf = _G["ChatFrame"..tostring(i)] + if cf then + self:EnforceChatWindowLock(cf) + if cf == activeChatFrame then + if cf.SetAlpha then cf:SetAlpha(1) end + if cf.EnableMouse then cf:EnableMouse(true) end + if cf.SetFrameLevel and self.frame and self.frame.inner then + cf:SetFrameLevel((self.frame.inner:GetFrameLevel() or self.frame:GetFrameLevel() or 1) + 6) + end + cf:Show() + self.chatFrame = cf + self.chatFrameIsCombat = isCombat + self:RefreshChatBounds() + else + cf:Hide() + if cf.SetAlpha then cf:SetAlpha(0) end + if cf.EnableMouse then cf:EnableMouse(false) end + if cf.SetFrameLevel and self.frame and self.frame.inner then + cf:SetFrameLevel(self.frame.inner:GetFrameLevel() or 1) + end + end + end + end +end + +function SFrames.Chat:AttachChatFrame() + if not (self.frame and self.frame.inner and ChatFrame1) then return end + self:SwitchActiveChatFrame(self:GetActiveTab()) +end + +function SFrames.Chat:RestoreCachedMessages(targetChatFrame) + if not targetChatFrame then return end + local cache = EnsureDB().messageCache + if not cache or table.getn(cache) == 0 then return end + + -- Find which frame index this is + local targetIdx = nil + for fi = 1, 7 do + if targetChatFrame == _G["ChatFrame" .. fi] then + targetIdx = fi + break + end + end + + local origAM = targetChatFrame.origAddMessage or targetChatFrame.AddMessage + if not origAM then return end + + for idx = 1, table.getn(cache) do + local entry = cache[idx] + if entry and entry.text then + local frameIdx = entry.frame or 1 + -- Only restore messages that belong to this frame + if frameIdx == targetIdx then + local msgID = entry.id or 0 + if not SFrames.Chat.MessageHistory then + SFrames.Chat.MessageHistory = {} + end + SFrames.Chat.MessageHistory[msgID] = entry.text + local modified = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. entry.text + origAM(targetChatFrame, modified, entry.r, entry.g, entry.b) + end + end + end +end + +function SFrames.Chat:ApplyAllTabsSetup() + local db = EnsureDB() + self.chatFrameToTabIndex = {} + for i = 1, table.getn(db.tabs) do + self:SetupChatFrameForTab(i) + self:ApplyTabFilters(i) + self:ApplyTabChannels(i) + end + -- Restore cached messages after filters cleared the frames + for i = 1, table.getn(db.tabs) do + local tab = db.tabs[i] + if tab then + local chatFrame = self:GetChatFrameForTab(tab) + if chatFrame then + self:RestoreCachedMessages(chatFrame) + end + end + end + self.cacheRestorePrimed = true + self:SwitchActiveChatFrame(self:GetActiveTab()) +end + +function SFrames.Chat:ApplyTabFilters(index) + local tab, idx = self:GetTab(index) + if not tab then return end + + local chatFrame, isCombat = self:GetChatFrameForTab(tab) + if not chatFrame then return end + + if tab.kind == "combat" and isCombat then + return + end + if not tab.filters then return end + + for _, group in ipairs(ALL_MESSAGE_GROUPS) do + pcall(function() + ChatFrame_RemoveMessageGroup(chatFrame, group) + end) + end + + local applied = {} + local anyEnabled = false + for _, def in ipairs(FILTER_DEFS) do + if tab.filters[def.key] ~= false then + anyEnabled = true + local groups = FILTER_GROUPS[def.key] or {} + for _, group in ipairs(groups) do + if not applied[group] then + applied[group] = true + pcall(function() + ChatFrame_AddMessageGroup(chatFrame, group) + end) + end + end + end + end + + if not anyEnabled then + tab.filters = CopyTable(DEFAULT_FILTERS) + applied = {} + for _, def in ipairs(FILTER_DEFS) do + if tab.filters[def.key] ~= false then + local groups = FILTER_GROUPS[def.key] or {} + for _, group in ipairs(groups) do + if not applied[group] then + applied[group] = true + pcall(function() + ChatFrame_AddMessageGroup(chatFrame, group) + end) + end + end + end + end + self:NotifyConfigUI() + end +end + +function SFrames.Chat:ApplyTabChannels(index) + local tab, idx = self:GetTab(index) + if not tab then return end + + local chatFrame, isCombat = self:GetChatFrameForTab(tab) + if not chatFrame then return end + if tab.kind == "combat" and isCombat then return end + + local channels = self:GetJoinedChannels() + self:ApplyBlockedChannelsGlobally(channels) + local addSupported = (ChatFrame_AddChannel ~= nil) + local removeSupported = (ChatFrame_RemoveChannel ~= nil) + if not (addSupported or removeSupported) then return end + if table.getn(channels) == 0 then return end + + for i = 1, table.getn(channels) do + local name = channels[i].name + local enabled = self:GetTabChannelFilter(idx, name) + if enabled then + if addSupported then + pcall(function() + ChatFrame_AddChannel(chatFrame, name) + end) + end + else + if removeSupported then + pcall(function() + ChatFrame_RemoveChannel(chatFrame, name) + end) + end + end + end +end + +function SFrames.Chat:ApplyAllTabChannels() + local db = EnsureDB() + for i = 1, table.getn(db.tabs) do + self:ApplyTabChannels(i) + end +end + +function SFrames.Chat:ApplyBlockedChannelsGlobally(channels) + if not ChatFrame_RemoveChannel then return end + channels = channels or self:GetJoinedChannels() + if table.getn(channels) == 0 then return end + + local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7 + local db = EnsureDB() + for i = 1, table.getn(channels) do + local info = channels[i] + local name = info and info.name + if IsIgnoredChannelByDefault(name) then + local anyTabEnabled = false + for t = 1, table.getn(db.tabs) do + if self:GetTabChannelFilter(t, name) then + anyTabEnabled = true + break + end + end + if not anyTabEnabled then + for w = 1, maxWindows do + local frame = _G["ChatFrame" .. tostring(w)] + if frame then + pcall(function() + ChatFrame_RemoveChannel(frame, name) + end) + end + end + end + end + end +end + +function SFrames.Chat:EnsureChannelMessageFilter() + return +end + +function SFrames.Chat:EnsureAddMessageChannelFilter() + return +end + +function SFrames.Chat:StartBlockedChannelWatcher() + local watcher = self.blockedChannelWatcher + if watcher and type(watcher) ~= "boolean" and watcher.SetScript then + watcher:SetScript("OnUpdate", nil) + end + self.blockedChannelWatcher = true +end + +function SFrames.Chat:RefreshTabButtons() + if not (self.frame and self.frame.tabBar) then return end + + local db = EnsureDB() + local tabs = db.tabs + local count = table.getn(tabs) + local activeIndex = self:GetActiveTabIndex() + local cfg = self:GetConfig() + local showBorder = (cfg.showBorder ~= false) + local borderR, borderG, borderB = self:GetBorderColorRGB() + local inactiveBorderA = showBorder and 0.72 or 0 + local activeBorderA = showBorder and 0.95 or 0 + local addBorderA = showBorder and 0.9 or 0 + + if not self.tabButtons then self.tabButtons = {} end + for i = 1, table.getn(self.tabButtons) do + self.tabButtons[i]:Hide() + end + + local function EnsureButtonSkin(btn) + if btn.sfSkinned then return end + + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + + if btn.SetBackdropColor then + btn:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.92) + end + if btn.SetBackdropBorderColor then + btn:SetBackdropBorderColor(borderR, borderG, borderB, inactiveBorderA) + end + + local bg = btn:CreateTexture(nil, "BACKGROUND") + bg:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background") + bg:SetPoint("TOPLEFT", btn, "TOPLEFT", 3, -3) + bg:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -3, 3) + bg:SetVertexColor(1, 0.62, 0.84, 0.12) + btn.sfBg = bg + + local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + local label = btn:CreateFontString(nil, "OVERLAY") + label:SetFont(fontPath, 9, "OUTLINE") + label:SetPoint("CENTER", btn, "CENTER", 0, 0) + label:SetTextColor(0.92, 0.84, 0.9) + btn.sfText = label + + btn.sfSkinned = true + end + + if not self.addTabButton then + local addBtn = CreateFrame("Button", nil, self.frame.tabBar) + addBtn:SetHeight(18) + addBtn:SetWidth(20) + addBtn:RegisterForClicks("LeftButtonUp") + EnsureButtonSkin(addBtn) + addBtn.sfText:SetText("+") + addBtn:SetScript("OnClick", function() + if SFrames and SFrames.Chat then + SFrames.Chat:PromptNewTab() + end + end) + addBtn:SetScript("OnMouseDown", function() + if this.sfBg then this.sfBg:SetVertexColor(1, 0.4, 0.7, 0.6) end + if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.btnDownBg[1], CFG_THEME.btnDownBg[2], CFG_THEME.btnDownBg[3], 0.96) end + end) + addBtn:SetScript("OnMouseUp", function() + if MouseIsOver and MouseIsOver(this) then + if this.sfBg then this.sfBg:SetVertexColor(1, 0.7, 0.9, 0.3) end + if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.btnHoverBg[1], CFG_THEME.btnHoverBg[2], CFG_THEME.btnHoverBg[3], 0.96) end + else + if this.sfBg then this.sfBg:SetVertexColor(1, 0.62, 0.84, 0.2) end + if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.94) end + end + end) + addBtn:SetScript("OnEnter", function() + if this.sfBg then this.sfBg:SetVertexColor(1, 0.7, 0.9, 0.3) end + if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.btnHoverBg[1], CFG_THEME.btnHoverBg[2], CFG_THEME.btnHoverBg[3], 0.96) end + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:ClearLines() + GameTooltip:AddLine("New Tab", 1, 0.84, 0.94) + GameTooltip:AddLine("Create chat tab", 0.85, 0.85, 0.85) + GameTooltip:Show() + end) + addBtn:SetScript("OnLeave", function() + if this.sfBg then this.sfBg:SetVertexColor(1, 0.62, 0.84, 0.2) end + if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.94) end + GameTooltip:Hide() + end) + self.addTabButton = addBtn + else + EnsureButtonSkin(self.addTabButton) + if self.addTabButton.sfText then self.addTabButton.sfText:SetText("+") end + end + + local gap = 3 + local addWidth = 20 + local barWidth = self.frame.tabBar:GetWidth() or 0 + if barWidth <= 0 then + barWidth = (self.frame:GetWidth() or DEFAULTS.width) - 180 + end + local available = barWidth - addWidth - gap * count - 2 + if available < 20 then available = 20 end + local buttonWidth = math.floor(available / math.max(1, count)) + buttonWidth = Clamp(buttonWidth, 14, 96) + local maxChars = math.max(3, math.floor((buttonWidth - 8) / 5)) + + local x = 0 + for i = 1, count do + local tab = tabs[i] + local btn = self.tabButtons[i] + if not btn then + btn = CreateFrame("Button", nil, self.frame.tabBar) + btn:SetHeight(18) + btn:RegisterForClicks("LeftButtonUp", "RightButtonUp") + self.tabButtons[i] = btn + end + + EnsureButtonSkin(btn) + + btn:ClearAllPoints() + btn:SetPoint("LEFT", self.frame.tabBar, "LEFT", x, 0) + btn:SetWidth(buttonWidth) + if btn.sfText then + btn.sfText:SetText(ShortText(tab.name or ("Tab" .. tostring(i)), maxChars)) + end + + local idx = i + btn:SetScript("OnClick", function() + if not (SFrames and SFrames.Chat) then return end + if arg1 == "RightButton" then + SFrames.Chat:OpenTabContextMenu(idx) + else + SFrames.Chat:SetActiveTab(idx) + end + end) + + btn:SetScript("OnEnter", function() + if idx ~= activeIndex then + if this.sfBg then this.sfBg:SetVertexColor(1, 0.68, 0.89, 0.25) end + if this.sfText then this.sfText:SetTextColor(0.92, 0.84, 0.92) end + if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.92) end + this:SetAlpha(0.96) + end + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:ClearLines() + GameTooltip:AddLine(tab.name or ("Tab" .. tostring(idx)), 1, 0.84, 0.94) + GameTooltip:AddLine("Left: switch", 0.82, 0.82, 0.82) + GameTooltip:AddLine("Right: menu", 1, 0.68, 0.79) + GameTooltip:Show() + end) + btn:SetScript("OnLeave", function() + if idx ~= activeIndex then + if this.sfBg then this.sfBg:SetVertexColor(1, 0.62, 0.84, 0.06) end + if this.sfText then this.sfText:SetTextColor(0.72, 0.64, 0.72) end + if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.78) end + this:SetAlpha(0.82) + end + GameTooltip:Hide() + end) + + if idx == activeIndex then + local aBg = CFG_THEME.tabActiveBg or CFG_THEME.buttonActiveBg or CFG_THEME.btnBg + local aBd = CFG_THEME.tabActiveBorder or CFG_THEME.buttonActiveBorder + if btn.SetBackdropColor then btn:SetBackdropColor(aBg[1], aBg[2], aBg[3], 0.98) end + if btn.SetBackdropBorderColor then + if aBd then + btn:SetBackdropBorderColor(aBd[1], aBd[2], aBd[3], 1) + else + btn:SetBackdropBorderColor(borderR, borderG, borderB, 1) + end + end + if btn.sfBg then btn.sfBg:SetVertexColor(1, 0.64, 0.86, 0.45) end + if btn.sfText then btn.sfText:SetTextColor(1, 1, 1) end + btn:SetAlpha(1) + else + if btn.SetBackdropColor then btn:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.78) end + if btn.SetBackdropBorderColor then btn:SetBackdropBorderColor(borderR, borderG, borderB, inactiveBorderA) end + if btn.sfBg then btn.sfBg:SetVertexColor(1, 0.62, 0.84, 0.06) end + if btn.sfText then btn.sfText:SetTextColor(0.72, 0.64, 0.72) end + btn:SetAlpha(0.82) + end + + btn:Show() + x = x + buttonWidth + gap + end + + self.addTabButton:ClearAllPoints() + self.addTabButton:SetPoint("LEFT", self.frame.tabBar, "LEFT", x, 0) + if self.addTabButton.SetBackdropColor then self.addTabButton:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.94) end + if self.addTabButton.SetBackdropBorderColor then self.addTabButton:SetBackdropBorderColor(borderR, borderG, borderB, addBorderA) end + if self.addTabButton.sfBg then self.addTabButton.sfBg:SetVertexColor(1, 0.62, 0.84, 0.2) end + if self.addTabButton.sfText then self.addTabButton.sfText:SetTextColor(1, 0.9, 0.98) end + self.addTabButton:Show() +end + + + +function SFrames.Chat:StyleChatFont() + if not (SFrames and SFrames.GetFont) then return end + local cfg = self:GetConfig() + local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" + local fontPath = SFrames:GetFont() + + local styled = {} + local function Apply(frame) + if not (frame and frame.SetFont) then return end + if styled[frame] then return end + styled[frame] = true + pcall(function() + frame:SetFont(fontPath, cfg.fontSize, outline) + end) + end + + if ChatFontNormal and ChatFontNormal.SetFont then + pcall(function() + ChatFontNormal:SetFont(fontPath, cfg.fontSize, outline) + end) + end + + Apply(self.chatFrame) + local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7 + for i = 1, maxWindows do + local cf = _G["ChatFrame"..tostring(i)] + if cf then Apply(cf) end + end +end + +function SFrames.Chat:HookChatEditColoring() + if ChatEdit_UpdateHeader and not self._colorHooked then + local orig = ChatEdit_UpdateHeader + ChatEdit_UpdateHeader = function(editBox) + orig(editBox) + + -- Re-apply insets to prevent buttons overlapping text and header + local header = _G[editBox:GetName().."Header"] + local hw = (header and header:GetWidth()) or 0 + if editBox and editBox.SetTextInsets then + -- Left: 20px (for cat icon) + header width + 5px padding + -- Right: 45px (for right shortcut buttons) + editBox:SetTextInsets(hw + 25, 45, 0, 0) + end + + local ctype = editBox.chatType + if not ctype and editBox.GetAttribute then + ctype = editBox:GetAttribute("chatType") + end + + local info = ChatTypeInfo[ctype] + if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then + if info then + SFrames.Chat.editBackdrop:SetBackdropBorderColor(info.r, info.g, info.b, 1) + if SFrames.Chat.editBackdrop.topLine then + SFrames.Chat.editBackdrop.topLine:SetVertexColor(info.r, info.g, info.b, 0.85) + end + if SFrames.Chat.editBackdrop.catIcon then + SFrames.Chat.editBackdrop.catIcon:SetVertexColor(info.r, info.g, info.b, 0.9) + end + else + -- Default coloring + local cfg = SFrames.Chat:GetConfig() + local r, g, b = SFrames.Chat:GetBorderColorRGB() + SFrames.Chat.editBackdrop:SetBackdropBorderColor(r, g, b, (cfg.showBorder ~= false) and 0.98 or 0) + if SFrames.Chat.editBackdrop.topLine then + SFrames.Chat.editBackdrop.topLine:SetVertexColor(r, g, b, 0.85) + end + if SFrames.Chat.editBackdrop.catIcon then + SFrames.Chat.editBackdrop.catIcon:SetVertexColor(1, 0.84, 0.94, 0.9) + end + end + end + end + self._colorHooked = true + end +end + +function SFrames.Chat:StyleEditBox() + if not self.frame then return end + + local editBox = ChatFrameEditBox or ChatFrame1EditBox + if not editBox then return end + self.editBox = editBox + + local cfg = self:GetConfig() + local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" + local fontPath = "Fonts\\ARIALN.TTF" + if SFrames and SFrames.GetFont then + local customFont = SFrames:GetFont() + if type(customFont) == "string" and customFont ~= "" then + fontPath = customFont + end + end + local editText = _G[editBox:GetName() .. "Text"] + + local ename = editBox:GetName() + if ename then + local suffixes = {"Left", "Mid", "Middle", "Right", "FocusLeft", "FocusMid", "FocusMiddle", "FocusRight", "Focus"} + for _, suf in ipairs(suffixes) do + local tex = _G[ename .. suf] + if tex then + tex:SetTexture(nil) + tex:Hide() + tex.Show = Dummy + end + end + end + + for _, texName in ipairs(EDITBOX_TEXTURES) do + local tex = _G[texName] + if tex then + tex:SetTexture(nil) + tex:Hide() + tex.Show = Dummy + end + end + + if editBox.SetBackdrop then + editBox:SetBackdrop(nil) + end + local regions = { editBox:GetRegions() } + for _, region in ipairs(regions) do + if region and region:GetObjectType() == "Texture" and region ~= editText then + local layer = region:GetDrawLayer() + if layer == "BACKGROUND" or layer == "BORDER" then + region:SetTexture(nil) + region:Hide() + end + end + end + + -- We will anchor editBox slightly later inside editBackdrop, but we set its basic properties here. + editBox:ClearAllPoints() + editBox:SetHeight(20) + if editBox.SetFrameStrata then editBox:SetFrameStrata("DIALOG") end + if editBox.SetFrameLevel then editBox:SetFrameLevel((self.frame:GetFrameLevel() or 1) + 20) end + if editBox.SetAltArrowKeyMode then editBox:SetAltArrowKeyMode(false) end + -- Let WoW manage text insets via ChatEdit_UpdateHeader dynamically + -- if editBox.SetTextInsets then editBox:SetTextInsets(20, 6, 0, 0) end + if editBox.SetJustifyH then editBox:SetJustifyH("LEFT") end + + local header = _G[editBox:GetName() .. "Header"] + if header then + header:ClearAllPoints() + header:SetPoint("LEFT", editBox, "LEFT", 20, 0) + header:Show() + end + + local function ApplyFontStyle(target) + if not target then return end + if target.SetFont then + pcall(function() + target:SetFont(fontPath, cfg.fontSize, outline) + end) + end + if target.SetFontObject and ChatFontNormal then + target:SetFontObject(ChatFontNormal) + end + if target.SetTextColor then target:SetTextColor(1, 1, 1) end + if target.SetShadowColor then target:SetShadowColor(0, 0, 0, 1) end + if target.SetShadowOffset then target:SetShadowOffset(1, -1) end + if target.SetAlpha then target:SetAlpha(1) end + end + + local function ApplyEditTextStyle() + ApplyFontStyle(editBox) + ApplyFontStyle(editText) + if header then ApplyFontStyle(header) end + if editText and editText.SetDrawLayer then editText:SetDrawLayer("OVERLAY", 7) end + if editText and editText.SetParent then editText:SetParent(editBox) end + if editText and editText.Show then editText:Show() end + end + ApplyEditTextStyle() + + if not self.editBackdrop then + local bg = CreateFrame("Frame", "SFramesChatEditBackdrop", self.frame) + bg:SetFrameStrata("DIALOG") + bg:SetFrameLevel(editBox:GetFrameLevel() - 1) + bg:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + bg:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.96) + bg:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0.98) + + local icon = SFrames:CreateIcon(bg, "logo", 13) + icon:SetPoint("LEFT", bg, "LEFT", 4, 0) + icon:SetVertexColor(1, 0.84, 0.94, 0.9) + bg.catIcon = icon + + local line = bg:CreateTexture(nil, "OVERLAY") + line:SetTexture("Interface\\Buttons\\WHITE8X8") + line:SetPoint("TOPLEFT", bg, "TOPLEFT", 1, -1) + line:SetPoint("TOPRIGHT", bg, "TOPRIGHT", -1, -1) + line:SetHeight(1) + line:SetVertexColor(1, 0.76, 0.9, 0.85) + line:Hide() + bg.topLine = line + + self.editBackdrop = bg + end + + -- Add shortcut buttons inside the edit backdrop's right side. + local buttonParent = self.editBackdrop or self.frame + local btnLevel = (buttonParent:GetFrameLevel() or editBox:GetFrameLevel() or 1) + 3 + if not self.rollButton then + local rb = CreateFrame("Button", "SFramesChatRollButton", buttonParent) + rb:SetWidth(18) + rb:SetHeight(18) + rb:SetFrameStrata("DIALOG") + rb:SetFrameLevel(btnLevel) + + local tex = rb:CreateTexture(nil, "ARTWORK") + tex:SetTexture("Interface\\Buttons\\UI-GroupLoot-Dice-Up") + tex:SetTexCoord(0.06, 0.94, 0.06, 0.94) + tex:SetAllPoints(rb) + rb:SetNormalTexture(tex) + + rb:SetScript("OnClick", function() RandomRoll(1, 100) end) + rb:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:SetText("掷骰子 (1-100)") + GameTooltip:Show() + end) + rb:SetScript("OnLeave", function() GameTooltip:Hide() end) + self.rollButton = rb + end + + if not self.emoteButton then + local eb = CreateFrame("Button", "SFramesChatEmoteButton", buttonParent) + eb:SetWidth(18) + eb:SetHeight(18) + eb:SetFrameStrata("DIALOG") + eb:SetFrameLevel(btnLevel) + + local tex = eb:CreateTexture(nil, "ARTWORK") + tex:SetTexture("Interface\\Icons\\Ability_Warrior_BattleShout") + tex:SetTexCoord(0.08, 0.92, 0.08, 0.92) + tex:SetAllPoints(eb) + eb:SetNormalTexture(tex) + + eb:SetScript("OnClick", function() + if not SFrames.Chat._emoteDropdown then + local dd = CreateFrame("Frame", "SFramesChatEmoteDropdown", UIParent, "UIDropDownMenuTemplate") + SFrames.Chat._emoteDropdown = dd + end + UIDropDownMenu_Initialize(SFrames.Chat._emoteDropdown, function() + local emotes = { + { text = "/大笑", cmd = "LAUGH" }, + { text = "/微笑", cmd = "SMILE" }, + { text = "/挥手", cmd = "WAVE" }, + { text = "/跳舞", cmd = "DANCE" }, + { text = "/鞠躬", cmd = "BOW" }, + { text = "/欢呼", cmd = "CHEER" }, + { text = "/感谢", cmd = "THANK" }, + { text = "/惊讶", cmd = "GASP" }, + { text = "/眨眼", cmd = "WINK" }, + { text = "/叹气", cmd = "SIGH" }, + { text = "/哭泣", cmd = "CRY" }, + { text = "/生气", cmd = "ANGRY" }, + { text = "/飞吻", cmd = "KISS" }, + { text = "/鼓掌", cmd = "APPLAUD"}, + { text = "/点头", cmd = "YES" }, + { text = "/摇头", cmd = "NO" }, + { text = "/强壮", cmd = "FLEX" }, + { text = "/哭喊", cmd = "WHINE" }, + } + for _, e in ipairs(emotes) do + local info = NewDropDownInfo() + info.text = e.text + info.notCheckable = 1 + local cmd = e.cmd + info.func = function() + DoEmote(cmd) + end + UIDropDownMenu_AddButton(info) + end + end, "MENU") + ToggleDropDownMenu(1, nil, SFrames.Chat._emoteDropdown, "SFramesChatEmoteButton", 0, 0) + end) + eb:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:SetText("表情动作") + GameTooltip:Show() + end) + eb:SetScript("OnLeave", function() GameTooltip:Hide() end) + self.emoteButton = eb + end + + -- Re-parent in case buttons were created by an older version. + self.rollButton:SetParent(self.editBackdrop) + self.emoteButton:SetParent(self.editBackdrop) + if self.rollButton.SetFrameLevel then + self.rollButton:SetFrameLevel((editBox:GetFrameLevel() or 1) + 2) + end + if self.emoteButton.SetFrameLevel then + self.emoteButton:SetFrameLevel((editBox:GetFrameLevel() or 1) + 2) + end + + -- Re-anchor buttons every call so they track editBackdrop position + self.rollButton:ClearAllPoints() + self.rollButton:SetPoint("RIGHT", self.editBackdrop, "RIGHT", -4, 0) + self.emoteButton:ClearAllPoints() + self.emoteButton:SetPoint("RIGHT", self.rollButton, "LEFT", -2, 0) + + local function SaveFreeEditBoxPosition() + if EnsureDB().editBoxPosition ~= "free" then return end + if not self.editBackdrop then return end + local x = self.editBackdrop:GetLeft() + local y = self.editBackdrop:GetBottom() + if x and y then + EnsureDB().editBoxX = x + EnsureDB().editBoxY = y + end + end + + self.editBackdrop:ClearAllPoints() + if cfg.editBoxPosition == "bottom" then + self.editBackdrop:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", cfg.sidePadding - 4, -5) + self.editBackdrop:SetPoint("TOPRIGHT", self.frame, "BOTTOMRIGHT", -(cfg.sidePadding - 4), -5) + elseif cfg.editBoxPosition == "free" then + self.editBackdrop:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", cfg.editBoxX or 0, cfg.editBoxY or 200) + self.editBackdrop:SetWidth(cfg.width - cfg.sidePadding * 2 + 8) + else -- "top" or default + self.editBackdrop:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", cfg.sidePadding - 4, 5) + self.editBackdrop:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cfg.sidePadding - 4), 5) + end + self.editBackdrop:SetHeight(26) + + self.editBackdrop:EnableMouse(true) + self.editBackdrop:SetMovable(true) + if self.editBackdrop.SetClampedToScreen then self.editBackdrop:SetClampedToScreen(true) end + self.editBackdrop:RegisterForDrag("LeftButton") + self.editBackdrop:SetScript("OnDragStart", function() + if IsAltKeyDown() and EnsureDB().editBoxPosition == "free" then + this:StartMoving() + end + end) + self.editBackdrop:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + SaveFreeEditBoxPosition() + end) + + editBox:SetParent(self.editBackdrop) + editBox:ClearAllPoints() + editBox:SetPoint("TOPLEFT", self.editBackdrop, "TOPLEFT", 4, -3) + editBox:SetPoint("BOTTOMRIGHT", self.editBackdrop, "BOTTOMRIGHT", -45, 3) + editBox:EnableMouse(true) + self.editBackdrop:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.96) + local borderR, borderG, borderB = self:GetBorderColorRGB() + self.editBackdrop:SetBackdropBorderColor(borderR, borderG, borderB, (cfg.showBorder ~= false) and 0.98 or 0) + if self.editBackdrop.topLine then + self.editBackdrop.topLine:Hide() + end + + if not editBox.sfNanamiHooked then + local oldOnShow = editBox:GetScript("OnShow") + local oldOnHide = editBox:GetScript("OnHide") + local oldOnTextChanged = editBox:GetScript("OnTextChanged") + + editBox:SetScript("OnShow", function() + if oldOnShow then oldOnShow() end + ApplyEditTextStyle() + if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then + SFrames.Chat.editBackdrop:Show() + end + end) + + editBox:SetScript("OnHide", function() + if oldOnHide then oldOnHide() end + if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then + SFrames.Chat.editBackdrop:Hide() + end + end) + + editBox:SetScript("OnTextChanged", function() + if oldOnTextChanged then oldOnTextChanged() end + ApplyEditTextStyle() + end) + + local origShow = editBox.Show + editBox.Show = function(self) + if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then + SFrames.Chat.editBackdrop:Show() + end + if origShow then origShow(self) end + end + + local origHide = editBox.Hide + editBox.Hide = function(self) + if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then + SFrames.Chat.editBackdrop:Hide() + end + if origHide then origHide(self) end + end + + editBox.sfNanamiHooked = true + end + + if not editBox.sfAltFreeDragHooked then + local oldMouseDown = editBox:GetScript("OnMouseDown") + local oldMouseUp = editBox:GetScript("OnMouseUp") + local oldDragStart = editBox:GetScript("OnDragStart") + local oldDragStop = editBox:GetScript("OnDragStop") + + editBox:RegisterForDrag("LeftButton") + editBox:SetScript("OnMouseDown", function() + if oldMouseDown then oldMouseDown() end + if IsAltKeyDown() and EnsureDB().editBoxPosition == "free" and SFrames.Chat and SFrames.Chat.editBackdrop and not SFrames.Chat._editBoxAltDragging then + SFrames.Chat._editBoxAltDragging = true + SFrames.Chat.editBackdrop:StartMoving() + end + end) + editBox:SetScript("OnMouseUp", function() + if oldMouseUp then oldMouseUp() end + if SFrames.Chat and SFrames.Chat._editBoxAltDragging and SFrames.Chat.editBackdrop then + SFrames.Chat.editBackdrop:StopMovingOrSizing() + SFrames.Chat._editBoxAltDragging = nil + SaveFreeEditBoxPosition() + end + end) + editBox:SetScript("OnDragStart", function() + if oldDragStart then oldDragStart() end + if IsAltKeyDown() and EnsureDB().editBoxPosition == "free" and SFrames.Chat and SFrames.Chat.editBackdrop and not SFrames.Chat._editBoxAltDragging then + SFrames.Chat._editBoxAltDragging = true + SFrames.Chat.editBackdrop:StartMoving() + end + end) + editBox:SetScript("OnDragStop", function() + if oldDragStop then oldDragStop() end + if SFrames.Chat and SFrames.Chat.editBackdrop then + SFrames.Chat.editBackdrop:StopMovingOrSizing() + SFrames.Chat._editBoxAltDragging = nil + SaveFreeEditBoxPosition() + end + end) + editBox.sfAltFreeDragHooked = true + end + + if editBox:IsShown() then + self.editBackdrop:Show() + else + self.editBackdrop:Hide() + end +end + +function SFrames.Chat:ApplyFrameBorderStyle() + if not self.frame then return end + + local cfg = self:GetConfig() + local showBorder = (cfg.showBorder ~= false) + local borderR, borderG, borderB = self:GetBorderColorRGB() + if showBorder then + local alpha = (SFrames and SFrames.isUnlocked) and 1 or 0.95 + self.frame:SetBackdropBorderColor(borderR, borderG, borderB, alpha) + else + self.frame:SetBackdropBorderColor(borderR, borderG, borderB, 0) + end + + if self.frame.topLine then + self.frame.topLine:Hide() + end + if self.frame.topGlow then + self.frame.topGlow:Hide() + end +end + +function SFrames.Chat:SetUnlocked(unlocked) + if not self.frame then return end + if self.frame.hint then + self.frame.hint:SetText("") + self.frame.hint:Hide() + end + self:ApplyFrameBorderStyle() + if not unlocked then + self:ApplyAllTabsSetup() + self:RefreshChatBounds() + end +end + +function SFrames.Chat:ApplyConfig() + if not self.frame then return end + + self:HookWindowDragBlocker() + + local db = EnsureDB() + local cfg = self:GetConfig() + + if cfg.enable == false then + if self.hiddenConfigButton then + self.hiddenConfigButton:Show() + end + self.frame:Hide() + if self.editBackdrop then self.editBackdrop:Hide() end + return + end + + if self.hiddenConfigButton then + self.hiddenConfigButton:Hide() + end + self.frame:Show() + self.frame:SetWidth(cfg.width) + self.frame:SetHeight(cfg.height) + self.frame:SetScale(cfg.scale) + + db.width = cfg.width + db.height = cfg.height + db.scale = cfg.scale + db.fontSize = cfg.fontSize + db.showBorder = cfg.showBorder + db.borderClassColor = cfg.borderClassColor + db.sidePadding = cfg.sidePadding + db.topPadding = cfg.topPadding + db.bottomPadding = cfg.bottomPadding + + if self.frame.tabBar and self.frame.title and self.frame.configButton then + self.frame.tabBar:ClearAllPoints() + self.frame.tabBar:SetPoint("LEFT", self.frame.title, "RIGHT", 10, -1) + self.frame.tabBar:SetPoint("RIGHT", self.frame.configButton, "LEFT", -8, -1) + self.frame.tabBar:SetHeight(18) + end + + if self.frame.inner then + self.frame.inner:ClearAllPoints() + self.frame.inner:SetPoint("TOPLEFT", self.frame, "TOPLEFT", cfg.sidePadding, -cfg.topPadding) + self.frame.inner:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", -cfg.sidePadding, cfg.bottomPadding) + end + + if self.frame.innerShade then + self.frame.innerShade:ClearAllPoints() + self.frame.innerShade:SetPoint("TOPLEFT", self.frame, "TOPLEFT", cfg.sidePadding - 2, -cfg.topPadding + 2) + self.frame.innerShade:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", -cfg.sidePadding + 2, cfg.bottomPadding + 2) + end + + local bgA = Clamp(cfg.bgAlpha or DEFAULTS.bgAlpha, 0, 1) + self.frame:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA) + + self:AttachChatFrame() + self:HideDefaultChrome() + self:HideTabChrome() + self:StyleChatFont() + self:RefreshTabButtons() + self:ApplyAllTabsSetup() + self:StyleEditBox() + self:RefreshChatBounds() + self:EnsureChromeWatcher() + self:StartStabilizer() + self:SetUnlocked(SFrames and SFrames.isUnlocked) + self:RefreshConfigFrame() +end + +function SFrames.Chat:Initialize() + EnsureDB() + self:CreateContainer() + self:HookWindowDragBlocker() + self:ApplyConfig() + self:StartPositionEnforcer() + self:EnsureAddMessageChannelFilter() + self:EnsureChannelMessageFilter() + self:ApplyBlockedChannelsGlobally() + self:StartBlockedChannelWatcher() + self:HookChatEditColoring() + + -- Hook ChatFrame_MessageEventHandler to suppress ignored channels (lft/lfg/etc) + if ChatFrame_MessageEventHandler and not SFrames.Chat._lftHooked then + local origHandler = ChatFrame_MessageEventHandler + ChatFrame_MessageEventHandler = function(frameArg, eventArg) + local frame = frameArg or this + local ev = eventArg or event + + if ev == "CHAT_MSG_CHANNEL" or ev == "CHAT_MSG_CHANNEL_JOIN" or ev == "CHAT_MSG_CHANNEL_LEAVE" or ev == "CHAT_MSG_CHANNEL_NOTICE" then + local chanName = GetChannelNameFromMessageEvent(arg4, arg8, arg9, arg2) + if chanName and chanName ~= "" then + if ev == "CHAT_MSG_CHANNEL" or ev == "CHAT_MSG_CHANNEL_JOIN" then + TrackDiscoveredChannel(chanName) + elseif ev == "CHAT_MSG_CHANNEL_LEAVE" then + UntrackDiscoveredChannel(chanName) + end + end + if frame and frame.GetName then + local frameName = frame:GetName() + if type(frameName) == "string" and string.find(frameName, "^ChatFrame%d+$") then + local matchedTabIdx = SFrames.Chat:GetTabIndexForChatFrame(frame) + if matchedTabIdx then + local tab = EnsureDB().tabs[matchedTabIdx] + local allowChannelMessages = not (tab.filters and tab.filters.channel == false) + if not allowChannelMessages or not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then + return + end + end + end + end + end + + return origHandler(frame, ev) + end + SFrames.Chat._lftHooked = true + end + + -- Direct event-based auto-translation hook (always installed as primary translation trigger) + if not SFrames.Chat._directTranslateHooked then + SFrames.Chat._directTranslateHooked = true + local translateEvFrame = CreateFrame("Frame", "SFramesChatTranslateEvents", UIParent) + for evName, _ in pairs(TRANSLATE_EVENT_FILTERS) do + translateEvFrame:RegisterEvent(evName) + end + translateEvFrame:SetScript("OnEvent", function() + if not (SFrames and SFrames.Chat) then return end + local filterKey = GetTranslateFilterKeyForEvent(event) + if not filterKey then return end + local messageText = arg1 + if type(messageText) ~= "string" or messageText == "" then return end + + local channelName = nil + if filterKey == "channel" then + channelName = GetChannelNameFromMessageEvent(arg4, arg8, arg9, arg2) + -- Note: do NOT skip ignored channels here — user may have explicitly + -- enabled them (e.g. hc/hardcore). ShouldAutoTranslateForTab will + -- correctly return false if the channel is not enabled for this tab. + end + + local db = EnsureDB() + local translated = false + for i = 1, table.getn(db.tabs) do + if not translated and SFrames.Chat:ShouldAutoTranslateForTab(i, filterKey, channelName) then + local tab = db.tabs[i] + if tab and type(tab.id) == "number" then + local cleanText = CleanTextForTranslation(messageText) + if cleanText ~= "" then + local tabId = tab.id + local senderName = arg2 + SFrames.Chat:RequestAutoTranslation(cleanText, function(result, err) + if result and result ~= "" then + SFrames.Chat:AppendAutoTranslatedLine(tabId, filterKey, channelName, cleanText, result, senderName) + end + end) + translated = true + end + end + end + end + end) + end + + SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() + if SFrames and SFrames.Chat then + SFrames.Chat:ApplyConfig() + SFrames.Chat:EnsureAddMessageChannelFilter() + SFrames.Chat:ApplyBlockedChannelsGlobally() + end + LoadPersistentClassCache() + if IsInGuild and IsInGuild() and GuildRoster then + GuildRoster() + end + SFrames:RefreshClassColorCache() + end) + + -- 团队成员变化时更新职业缓存 + SFrames:RegisterEvent("PARTY_MEMBERS_CHANGED", function() SFrames:RefreshClassColorCache() end) + SFrames:RegisterEvent("RAID_ROSTER_UPDATE", function() SFrames:RefreshClassColorCache() end) + SFrames:RegisterEvent("FRIENDLIST_UPDATE", function() SFrames:RefreshClassColorCache() end) + SFrames:RegisterEvent("GUILD_ROSTER_UPDATE", function() SFrames:RefreshClassColorCache() end) + SFrames:RegisterEvent("WHO_LIST_UPDATE", function() SFrames:RefreshClassColorCache() end) + + -- 监听公会/队伍聊天,未命中缓存时即时刷新名单 + if not SFrames.Chat._classRefreshHooked then + SFrames.Chat._classRefreshHooked = true + local classRefreshFrame = CreateFrame("Frame", "SFramesChatClassRefresh", UIParent) + classRefreshFrame:RegisterEvent("CHAT_MSG_GUILD") + classRefreshFrame:RegisterEvent("CHAT_MSG_OFFICER") + classRefreshFrame:RegisterEvent("CHAT_MSG_PARTY") + classRefreshFrame:RegisterEvent("CHAT_MSG_RAID") + classRefreshFrame:RegisterEvent("CHAT_MSG_RAID_LEADER") + classRefreshFrame:SetScript("OnEvent", function() + local sender = arg2 + if sender and sender ~= "" and not SFrames.PlayerClassColorCache[sender] then + if event == "CHAT_MSG_GUILD" or event == "CHAT_MSG_OFFICER" then + if GuildRoster then GuildRoster() end + else + SFrames:RefreshClassColorCache() + end + end + end) + end + + SFrames:RegisterEvent("UPDATE_CHAT_WINDOWS", function() + if SFrames and SFrames.Chat then + if EnsureDB().enable == false then return end + SFrames.Chat:HideDefaultChrome() + SFrames.Chat:HideTabChrome() + SFrames.Chat:RefreshChatBounds() + SFrames.Chat:RefreshTabButtons() + SFrames.Chat:StyleEditBox() + SFrames.Chat:EnsureAddMessageChannelFilter() + SFrames.Chat:ApplyBlockedChannelsGlobally() + SFrames.Chat:RefreshConfigFrame() + end + end) + + SFrames:RegisterEvent("UPDATE_CHAT_COLOR", function() + if SFrames and SFrames.Chat then + if EnsureDB().enable == false then return end + SFrames.Chat:StyleChatFont() + SFrames.Chat:StyleEditBox() + end + end) + + if not SFrames.Chat._msgHooked then + SFrames.Chat._msgHooked = true + + local MAX_CACHE = 200 + + -- Initialize from persistent storage + local db = EnsureDB() + if type(db.messageCache) ~= "table" then + db.messageCache = {} + end + + -- MessageHistory is the live runtime lookup (msgID -> raw text), seeded from cache + SFrames.Chat.MessageHistory = {} + SFrames.Chat.MessageSenders = {} + SFrames.Chat.MessageIndex = 0 + + -- Seed runtime lookup from persistent cache + for i = 1, table.getn(db.messageCache) do + local entry = db.messageCache[i] + if entry and entry.id and entry.text then + SFrames.Chat.MessageHistory[entry.id] = entry.text + if entry.id > SFrames.Chat.MessageIndex then + SFrames.Chat.MessageIndex = entry.id + end + end + end + + -- Helper: save a message to the persistent cache ring buffer + local function PersistMessage(msgID, text, r, g, b, frameIndex) + local cache = EnsureDB().messageCache + if not cache then + EnsureDB().messageCache = {} + cache = EnsureDB().messageCache + end + table.insert(cache, { + id = msgID, + text = text, + r = r, + g = g, + b = b, + frame = frameIndex or 1, + time = date("%H:%M:%S"), + }) + -- Trim to MAX_CACHE + while table.getn(cache) > MAX_CACHE do + table.remove(cache, 1) + end + end + + for i = 1, 7 do + local cf = _G["ChatFrame" .. i] + if cf and cf.AddMessage then + local origAddMessage = cf.AddMessage + cf.origAddMessage = origAddMessage + cf.AddMessage = function(self, text, r, g, b, alpha, holdTime) + if not text or text == "" then + origAddMessage(self, text, r, g, b, alpha, holdTime) + return + end + if string.sub(text, 1, 10) ~= "|Hsfchat:" and string.sub(text, 1, 12) ~= "|cff888888|H" then + local db = EnsureDB() + -- Universal catch for Turtle WoW custom chat channels like [硬核] + local chanName = GetChannelNameFromChatLine(text) + + if chanName and IsIgnoredChannelByDefault(chanName) then + -- Global HC kill switch override check + if db.hcGlobalDisable then + local lowerChan = string.lower(chanName) + if string.find(lowerChan, "hc") or string.find(chanName, "硬核") or string.find(lowerChan, "hardcore") then + return + end + end + + local frameName = self:GetName() + local matchedTabIdx = nil + if type(frameName) == "string" and string.find(frameName, "^ChatFrame%d+$") then + matchedTabIdx = SFrames.Chat:GetTabIndexForChatFrame(self) + if not matchedTabIdx then + local _, _, frameNumStr = string.find(frameName, "^ChatFrame(%d+)$") + local frameNum = tonumber(frameNumStr) + if frameNum then + local db = EnsureDB() + if frameNum <= table.getn(db.tabs) then + matchedTabIdx = frameNum + end + end + end + end + if matchedTabIdx then + local tab = EnsureDB().tabs[matchedTabIdx] + if tab then + if not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then + return + end + -- If it passed channel filter, allow translation for these bypassed channels + local shouldTranslate = SFrames.Chat:GetTabChannelTranslateFilter(matchedTabIdx, chanName) + if shouldTranslate then + local cleanText = CleanTextForTranslation(text) + -- Remove the channel prefix from translation text to be clean + cleanText = string.gsub(cleanText, "^%[.-%]%s*", "") + -- It might also have [PlayerName]: + local _, _, senderName = string.find(cleanText, "^%[([^%]]+)%]:%s*") + if senderName then + cleanText = string.gsub(cleanText, "^%[[^%]]+%]:%s*", "") + end + if cleanText ~= "" then + local tabId = tab.id + SFrames.Chat:RequestAutoTranslation(cleanText, function(result) + if result and result ~= "" then + SFrames.Chat:AppendAutoTranslatedLine(tabId, "channel", chanName, cleanText, result, senderName) + end + end) + end + end + end + else + return + end + end + + -- Hardcore Death Event Overrides + if db.hcDeathDisable or (db.hcDeathLevelMin and db.hcDeathLevelMin > 1) then + local deathLvl = ParseHardcoreDeathMessage(text) + if deathLvl then + if db.hcDeathDisable then return end + if db.hcDeathLevelMin and deathLvl < db.hcDeathLevelMin then return end + end + end + + SFrames.Chat.MessageIndex = SFrames.Chat.MessageIndex + 1 + local msgID = SFrames.Chat.MessageIndex + + -- Store in runtime lookup + SFrames.Chat.MessageHistory[msgID] = text + + -- Persist to SavedVariables + local frameIdx = nil + for fi = 1, 7 do + if self == _G["ChatFrame" .. fi] then + frameIdx = fi + break + end + end + PersistMessage(msgID, text, r, g, b, frameIdx) + + -- Apply class color to player names in message + local coloredText = ColorPlayerNamesInText(text) + -- Insert the clickable button [+] at the beginning + local modifiedText = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. coloredText + origAddMessage(self, modifiedText, r, g, b, alpha, holdTime) + + -- Legacy auto-translate disabled; handled by per-tab routing above. + if false then + if self == ChatFrame1 and not string.find(text, "%[翻译%]") then + if string.find(text, "%[.-硬核.-%]") or string.find(string.lower(text), "%[.-hc.-%]") or string.find(string.lower(text), "%[.-hardcore.-%]") then + local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") + cleanText = string.gsub(cleanText, "|r", "") + cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1") + + if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then + _G.STranslateAPI.Translate(cleanText, "auto", "zh", function(result, err, meta) + if result then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[翻译] |cffffff00" .. tostring(result) .. "|r") + end + end, "Nanami-UI") + elseif _G.STranslate and _G.STranslate.SendIO then + _G.STranslate:SendIO(cleanText, "IN", "auto", "zh") + end + end + end + end + else + origAddMessage(self, text, r, g, b, alpha, holdTime) + end + end + end + end + + -- Restore cached messages after a short delay so chat frames are fully ready + if not SFrames.Chat._cacheRestored then + SFrames.Chat._cacheRestored = true + local restoreFrame = CreateFrame("Frame", nil, UIParent) + restoreFrame.elapsed = 0 + restoreFrame:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + arg1 + if this.elapsed < 1.5 then return end + this:SetScript("OnUpdate", nil) + this:Hide() + + if SFrames.Chat.cacheRestorePrimed then + return + end + + local cache = EnsureDB().messageCache + if not cache or table.getn(cache) == 0 then return end + + -- Split cache restoration across multiple frames to prevent WoW from freezing + local currentIdx = 1 + local totalCount = table.getn(cache) + + this:SetScript("OnUpdate", function() + local chunkEnd = math.min(currentIdx + 15, totalCount) + for idx = currentIdx, chunkEnd do + local entry = cache[idx] + if entry and entry.text then + local frameIdx = entry.frame or 1 + local cf = _G["ChatFrame" .. frameIdx] + if not cf then cf = ChatFrame1 end + if cf and cf.AddMessage then + local msgID = entry.id or 0 + SFrames.Chat.MessageHistory[msgID] = entry.text + local modified = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. entry.text + -- Call with the raw origAddMessage to avoid re-persisting + local origAM = cf.origAddMessage or cf.AddMessage + origAM(cf, modified, entry.r, entry.g, entry.b) + end + end + end + currentIdx = chunkEnd + 1 + if currentIdx > totalCount then + this:SetScript("OnUpdate", nil) + this:Hide() + SFrames.Chat.cacheRestorePrimed = true + end + end) + end) + end + end + + if not SFrames.Chat._itemRefHooked then + SFrames.Chat._itemRefHooked = true + local origSetItemRef = SetItemRef + SetItemRef = function(link, text, button) + if link and string.sub(link, 1, 7) == "sfchat:" then + local msgID = tonumber(string.sub(link, 8)) + if msgID and SFrames.Chat.MessageHistory[msgID] then + local messageText = SFrames.Chat.MessageHistory[msgID] + local sender = nil + + -- Check MessageSenders lookup first (for AI translated messages) + if SFrames.Chat.MessageSenders and SFrames.Chat.MessageSenders[msgID] then + sender = SFrames.Chat.MessageSenders[msgID] + else + -- Attempt to extract sender name from raw text like: |Hplayer:Name|h[Name]|h or [Name]: + local _, _, extractedName = string.find(messageText, "|Hplayer:(.-)|h") + if extractedName then + sender = string.gsub(extractedName, ":.*", "") + else + _, _, sender = string.find(messageText, "%[(.-)%]") + end + end + -- `this` inside SetItemRef is usually the ChatFrame that was clicked + local clickedFrame = this + if type(clickedFrame) ~= "table" or not clickedFrame.AddMessage then + local activeIdx = SFrames.Chat:GetActiveTabIndex() + local tab = SFrames.Chat:GetTab(activeIdx) + clickedFrame = tab and SFrames.Chat:GetChatFrameForTab(tab) or DEFAULT_CHAT_FRAME + end + + SFrames.Chat:OpenMessageContextMenu(msgID, messageText, sender, clickedFrame) + end + return + end + if origSetItemRef then + origSetItemRef(link, text, button) + end + end + end +end + +function SFrames.Chat:ShowCopyDialog(text) + if not self.copyFrame then + local f = CreateFrame("Frame", "SFramesChatCopyFrame", UIParent) + f:SetWidth(400) + f:SetHeight(150) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 100) + f:SetFrameStrata("FULLSCREEN_DIALOG") + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + if SFrames and SFrames.CreateBackdrop then + SFrames:CreateBackdrop(f) + else + f:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = true, tileSize = 32, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + end + f:SetBackdropColor(0.08, 0.07, 0.1, 0.96) + f:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8) + + local close = CreateFrame("Button", nil, f, "UIPanelCloseButton") + close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -2, -2) + + local l = f:CreateFontString(nil, "OVERLAY") + l:SetFont("Fonts\\ARIALN.TTF", 12, "OUTLINE") + l:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -10) + l:SetText("复制聊天内容 (Ctrl+C)") + + local sf = CreateFrame("ScrollFrame", "SFramesChatCopyScroll", f, "UIPanelScrollFrameTemplate") + sf:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -30) + sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -30, 10) + + local edit = CreateFrame("EditBox", "SFramesChatCopyEdit", sf) + edit:SetWidth(350) + edit:SetHeight(90) + edit:SetMultiLine(true) + edit:SetFont("Fonts\\ARIALN.TTF", 12, "OUTLINE") + edit:SetAutoFocus(false) + edit:SetScript("OnEscapePressed", function() f:Hide() end) + sf:SetScrollChild(edit) + + f.edit = edit + self.copyFrame = f + end + + local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") + cleanText = string.gsub(cleanText, "|r", "") + cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1") + + self.copyFrame.edit:SetText(cleanText) + self.copyFrame:Show() + self.copyFrame.edit:HighlightText() + self.copyFrame.edit:SetFocus() +end + +function SFrames.Chat:OpenMessageContextMenu(msgID, text, sender, targetFrame) + if not self._msgDropdown then + self._msgDropdown = CreateFrame("Frame", "SFramesChatMessageDropdown", UIParent, "UIDropDownMenuTemplate") + self._msgDropdown:SetFrameStrata("FULLSCREEN_DIALOG") + end + UIDropDownMenu_Initialize(self._msgDropdown, function() + local level = UIDROPDOWNMENU_MENU_LEVEL or 1 + + local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") + cleanText = string.gsub(cleanText, "|r", "") + cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1") + + if level == 2 and UIDROPDOWNMENU_MENU_VALUE == "STranslate_Langs" then + local langs = nil + if _G.STranslateAPI and _G.STranslateAPI.GetSupportedLanguages then + langs = _G.STranslateAPI.GetSupportedLanguages() + end + + if type(langs) ~= "table" or table.getn(langs) == 0 then + langs = { + { code = "zh", name = "中文" }, + { code = "en", name = "英语" }, + { code = "es", name = "西班牙语" }, + { code = "fr", name = "法语" }, + { code = "pl", name = "波兰语" }, + { code = "de", name = "德语" }, + { code = "ru", name = "俄语" }, + { code = "ko", name = "韩语" }, + } + end + + local langNames = { + zh = "中文", + en = "英语", + es = "西班牙语", + fr = "法语", + pl = "波兰语", + de = "德语", + ru = "俄语", + ko = "韩语", + ja = "日语", + auto = "自动检测" + } + + for _, lang in ipairs(langs) do + local info = NewDropDownInfo() + info.text = langNames[lang.code] or lang.name or lang.code + info.notCheckable = 1 + + -- Capture the primitive value correctly for Lua 5.0 loops + local currentLangCode = lang.code + + info.func = function() + local outputFrame = targetFrame or DEFAULT_CHAT_FRAME + if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then + _G.STranslateAPI.Translate(cleanText, "auto", currentLangCode, function(result, err, meta) + if err then + outputFrame:AddMessage("|cffff3333[Nanami-UI] 翻译失败:|r " .. tostring(err)) + return + end + if not result then return end + outputFrame:AddMessage("|cff00ffff[翻译] |cffffff00" .. tostring(result) .. "|r") + end, "Nanami-UI") + elseif _G.STranslate and _G.STranslate.SendIO then + _G.STranslate:SendIO(cleanText, "IN", "auto", currentLangCode) + end + CloseDropDownMenus() + end + UIDropDownMenu_AddButton(info, level) + end + return + end + + if level == 1 then + if sender and sender ~= "" then + local replySender = sender + local infoReply = NewDropDownInfo() + infoReply.text = "回复 " .. replySender + infoReply.notCheckable = 1 + infoReply.func = function() + if ChatFrameEditBox then + ChatFrameEditBox:Show() + ChatFrameEditBox:SetText("/w " .. replySender .. " ") + ChatFrameEditBox:SetFocus() + end + CloseDropDownMenus() + end + UIDropDownMenu_AddButton(infoReply, level) + + local infoName = NewDropDownInfo() + infoName.text = "复制玩家姓名" + infoName.notCheckable = 1 + infoName.func = function() + SFrames.Chat:ShowCopyDialog(replySender) + end + UIDropDownMenu_AddButton(infoName, level) + end + + local info = NewDropDownInfo() + info.text = "复制内容" + info.notCheckable = 1 + info.func = function() + SFrames.Chat:ShowCopyDialog(text) + end + UIDropDownMenu_AddButton(info, level) + + if (_G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady()) or (_G.STranslate and _G.STranslate.SendIO) then + local t = NewDropDownInfo() + t.text = "翻译为..." + t.hasArrow = 1 + t.value = "STranslate_Langs" + t.notCheckable = 1 + UIDropDownMenu_AddButton(t, level) + end + end + end, "MENU") + ToggleDropDownMenu(1, nil, self._msgDropdown, "cursor", 3, -3) +end + SFrames:RegisterEvent("CHANNEL_UI_UPDATE", function() + if SFrames and SFrames.Chat then + if EnsureDB().enable == false then return end + SFrames.Chat:ApplyBlockedChannelsGlobally() + SFrames.Chat:RefreshConfigFrame() + local delayFrame = CreateFrame("Frame") + local elapsed = 0 + delayFrame:SetScript("OnUpdate", function() + elapsed = elapsed + (arg1 or 0) + if elapsed >= 0.5 then + this:SetScript("OnUpdate", nil) + if SFrames and SFrames.Chat then + SFrames.Chat:RefreshConfigFrame() + end + end + end) + end + end) + + SFrames:RegisterEvent("CHAT_MSG_CHANNEL_NOTICE", function() + if SFrames and SFrames.Chat then + if EnsureDB().enable == false then return end + -- Track channel join/leave from notice events (arg9 = channel name) + local noticeName = arg9 or arg4 or "" + if noticeName ~= "" then + if arg1 == "YOU_JOINED" then + TrackDiscoveredChannel(noticeName) + elseif arg1 == "YOU_LEFT" then + UntrackDiscoveredChannel(noticeName) + end + end + SFrames.Chat:ApplyBlockedChannelsGlobally() + SFrames.Chat:RefreshConfigFrame() + -- Delayed re-refresh: GetChannelList() may lag behind the event + local delayFrame = CreateFrame("Frame") + local elapsed = 0 + delayFrame:SetScript("OnUpdate", function() + elapsed = elapsed + (arg1 or 0) + if elapsed >= 0.5 then + this:SetScript("OnUpdate", nil) + if SFrames and SFrames.Chat then + SFrames.Chat:RefreshConfigFrame() + end + end + end) + end + end) + + SFrames:RegisterEvent("ZONE_CHANGED", function() + if SFrames and SFrames.Chat then + if EnsureDB().enable == false then return end + SFrames.Chat:ApplyBlockedChannelsGlobally() + SFrames.Chat:RefreshConfigFrame() + SFrames.Chat:StartStabilizer() + end + end) + + SFrames:RegisterEvent("ZONE_CHANGED_INDOORS", function() + if SFrames and SFrames.Chat then + if EnsureDB().enable == false then return end + SFrames.Chat:ApplyBlockedChannelsGlobally() + SFrames.Chat:RefreshConfigFrame() + SFrames.Chat:StartStabilizer() + end + end) + + SFrames:RegisterEvent("ZONE_CHANGED_NEW_AREA", function() + if SFrames and SFrames.Chat then + if EnsureDB().enable == false then return end + SFrames.Chat:ApplyBlockedChannelsGlobally() + SFrames.Chat:RefreshConfigFrame() + end + end) + + local function ChatCombatReanchor() + if SFrames and SFrames.Chat then + if EnsureDB().enable == false then return end + SFrames.Chat:ReanchorChatFrames() + end + end + SFrames:RegisterEvent("PLAYER_REGEN_DISABLED", ChatCombatReanchor) + SFrames:RegisterEvent("PLAYER_REGEN_ENABLED", ChatCombatReanchor) + diff --git a/ClassSkillData.lua b/ClassSkillData.lua new file mode 100644 index 0000000..3c6001f --- /dev/null +++ b/ClassSkillData.lua @@ -0,0 +1,346 @@ +SFrames.ClassSkillData = { + WARRIOR = { + [4] = {"冲锋", "撕裂"}, + [6] = {"雷霆一击"}, + [8] = {"英勇打击 2级", "断筋"}, + [10] = {"撕裂 2级", "血性狂暴"}, + [12] = {"压制", "盾击", "战斗怒吼 2级"}, + [14] = {"挫志怒吼", "复仇"}, + [16] = {"英勇打击 3级", "惩戒痛击", "盾牌格挡"}, + [18] = {"雷霆一击 2级", "缴械"}, + [20] = {"撕裂 3级", "反击风暴", "顺劈斩"}, + [22] = {"战斗怒吼 3级", "破甲攻击 2级", "破胆怒吼"}, + [24] = {"英勇打击 4级", "挫志怒吼 2级", "复仇 2级", "斩杀"}, + [26] = {"冲锋 2级", "惩戒痛击 2级", "挑战怒吼"}, + [28] = {"雷霆一击 3级", "压制 2级", "盾墙"}, + [30] = {"撕裂 4级", "顺劈斩 2级", "猛击", "狂暴姿态"}, + [32] = {"英勇打击 5级", "断筋 2级", "斩杀 2级", "战斗怒吼 4级", "盾击 2级", "狂暴之怒"}, + [34] = {"挫志怒吼 3级", "复仇 3级", "破甲攻击 3级"}, + [36] = {"惩戒痛击 3级", "旋风斩"}, + [38] = {"雷霆一击 4级", "猛击 2级"}, + [40] = {"英勇打击 6级", "撕裂 5级", "顺劈斩 3级", "斩杀 3级"}, + [42] = {"战斗怒吼 5级", "拦截 2级"}, + [44] = {"压制 3级", "挫志怒吼 4级", "复仇 4级"}, + [46] = {"冲锋 3级", "惩戒痛击 4级", "猛击 3级", "破甲攻击 4级"}, + [48] = {"英勇打击 7级", "雷霆一击 5级", "斩杀 4级"}, + [50] = {"撕裂 6级", "鲁莽", "顺劈斩 4级"}, + [52] = {"战斗怒吼 6级", "拦截 3级", "盾击 3级"}, + [54] = {"断筋 3级", "挫志怒吼 5级", "猛击 4级", "复仇 5级"}, + [56] = {"英勇打击 8级", "惩戒痛击 5级", "斩杀 5级"}, + [58] = {"雷霆一击 6级", "破甲攻击 5级"}, + [60] = {"撕裂 7级", "压制 4级", "顺劈斩 5级"}, + }, + PALADIN = { + [4] = {"力量祝福", "审判"}, + [6] = {"圣光术 2级", "圣佑术", "十字军圣印"}, + [8] = {"纯净术", "制裁之锤"}, + [10] = {"圣疗术", "正义圣印 2级", "虔诚光环 2级", "保护祝福"}, + [12] = {"力量祝福 2级", "十字军圣印 2级"}, + [14] = {"圣光术 3级"}, + [16] = {"正义之怒", "惩罚光环"}, + [18] = {"正义圣印 3级", "圣佑术 2级"}, + [20] = {"驱邪术", "圣光闪现", "虔诚光环 3级"}, + [22] = {"圣光术 4级", "专注光环", "公正圣印", "力量祝福 3级", "十字军圣印 3级"}, + [24] = {"超度亡灵", "救赎 2级", "智慧祝福 2级", "制裁之锤 2级", "保护祝福 2级"}, + [26] = {"圣光闪现 2级", "正义圣印 4级", "拯救祝福", "惩罚光环 2级"}, + [28] = {"驱邪术 2级"}, + [30] = {"圣疗术 2级", "圣光术 5级", "光明圣印", "虔诚光环 4级", "神圣干涉"}, + [32] = {"冰霜抗性光环", "力量祝福 4级", "十字军圣印 4级"}, + [34] = {"智慧祝福 3级", "圣光闪现 3级", "正义圣印 5级", "圣盾术"}, + [36] = {"驱邪术 3级", "救赎 3级", "火焰抗性光环", "惩罚光环 3级"}, + [38] = {"圣光术 6级", "超度亡灵 2级", "智慧圣印", "保护祝福 3级"}, + [40] = {"光明祝福", "光明圣印 2级", "虔诚光环 5级", "制裁之锤 3级"}, + [42] = {"圣光闪现 4级", "正义圣印 6级", "力量祝福 5级", "十字军圣印 5级"}, + [44] = {"驱邪术 4级", "智慧祝福 4级", "冰霜抗性光环 2级"}, + [46] = {"圣光术 7级", "惩罚光环 4级"}, + [48] = {"救赎 4级", "智慧圣印 2级", "火焰抗性光环 2级"}, + [50] = {"圣疗术 3级", "圣光闪现 5级", "光明祝福 2级", "光明圣印 3级", "正义圣印 7级", "虔诚光环 6级", "圣盾术 2级"}, + [52] = {"驱邪术 5级", "超度亡灵 3级", "力量祝福 6级", "十字军圣印 6级", "强效力量祝福"}, + [54] = {"圣光术 8级", "智慧祝福 5级", "强效智慧祝福", "制裁之锤 4级"}, + [56] = {"冰霜抗性光环 3级", "惩罚光环 5级"}, + [58] = {"圣光闪现 6级", "智慧圣印 3级", "正义圣印 8级"}, + [60] = {"驱邪术 6级", "救赎 5级", "光明祝福 3级", "光明圣印 4级", "强效光明祝福", "虔诚光环 7级", "火焰抗性光环 3级", "强效力量祝福 2级"}, + }, + HUNTER = { + [4] = {"灵猴守护", "毒蛇钉刺"}, + [6] = {"猎人印记", "奥术射击"}, + [8] = {"震荡射击", "猛禽一击 2级"}, + [10] = {"雄鹰守护", "毒蛇钉刺 2级", "持久耐力", "自然护甲", "追踪人型生物"}, + [12] = {"治疗宠物", "奥术射击 2级", "扰乱射击", "摔绊"}, + [14] = {"野兽之眼", "恐吓野兽", "鹰眼术"}, + [16] = {"猛禽一击 3级", "献祭陷阱", "猫鼬撕咬"}, + [18] = {"雄鹰守护 2级", "毒蛇钉刺 3级", "追踪亡灵", "多重射击"}, + [20] = {"治疗宠物 2级", "猎豹守护", "奥术射击 3级", "逃脱", "冰冻陷阱", "猛禽一击 4级"}, + [22] = {"猎人印记 2级", "毒蝎钉刺"}, + [24] = {"野兽知识", "追踪隐藏生物"}, + [26] = {"毒蛇钉刺 4级", "急速射击", "追踪元素生物", "献祭陷阱 2级"}, + [28] = {"治疗宠物 3级", "雄鹰守护 3级", "奥术射击 4级", "冰霜陷阱"}, + [30] = {"恐吓野兽 2级", "野兽守护", "多重射击 2级", "猫鼬撕咬 2级", "假死"}, + [32] = {"照明弹", "爆炸陷阱", "追踪恶魔", "猛禽一击 5级"}, + [34] = {"毒蛇钉刺 5级", "逃脱 2级"}, + [36] = {"治疗宠物 4级", "蝰蛇钉刺", "献祭陷阱 3级"}, + [38] = {"雄鹰守护 4级"}, + [40] = {"豹群守护", "猎人印记 3级", "乱射", "扰乱射击 4级", "冰冻陷阱 2级", "猛禽一击 6级", "追踪巨人"}, + [42] = {"毒蛇钉刺 6级", "多重射击 3级"}, + [44] = {"治疗宠物 5级", "奥术射击 6级", "爆炸陷阱 2级", "献祭陷阱 4级", "猫鼬撕咬 3级"}, + [46] = {"恐吓野兽 3级", "蝰蛇钉刺 2级"}, + [48] = {"雄鹰守护 5级", "猛禽一击 7级", "逃脱 3级"}, + [50] = {"毒蛇钉刺 7级", "乱射 2级", "追踪龙类"}, + [52] = {"治疗宠物 6级", "毒蝎钉刺 4级"}, + [54] = {"多重射击 4级", "爆炸陷阱 3级", "猫鼬撕咬 4级", "猛禽一击 8级"}, + [56] = {"蝰蛇钉刺 3级", "献祭陷阱 5级"}, + [58] = {"猎人印记 4级", "乱射 3级", "毒蛇钉刺 8级", "雄鹰守护 6级"}, + [60] = {"治疗宠物 7级", "奥术射击 8级", "扰乱射击 6级", "冰冻陷阱 3级", "摔绊 3级"}, + }, + ROGUE = { + [4] = {"背刺", "搜索"}, + [6] = {"邪恶攻击 2级", "凿击"}, + [8] = {"刺骨 2级", "闪避"}, + [10] = {"切割", "疾跑", "闷棍"}, + [12] = {"背刺 2级", "脚踢"}, + [14] = {"绞喉", "破甲", "邪恶攻击 3级"}, + [16] = {"刺骨 3级", "佯攻"}, + [18] = {"凿击 2级", "伏击"}, + [20] = {"割裂", "背刺 3级", "潜行 2级", "致残毒药"}, + [22] = {"绞喉 2级", "邪恶攻击 4级", "扰乱", "消失"}, + [24] = {"刺骨 4级", "麻痹毒药", "侦测陷阱"}, + [26] = {"偷袭", "破甲 2级", "伏击 2级", "脚踢 2级"}, + [28] = {"割裂 2级", "背刺 4级", "佯攻 2级", "闷棍 2级"}, + [30] = {"绞喉 3级", "邪恶攻击 5级", "肾击", "致命毒药"}, + [32] = {"凿击 3级", "致伤毒药"}, + [34] = {"疾跑 2级"}, + [36] = {"割裂 3级", "破甲 3级"}, + [38] = {"绞喉 4级", "致命毒药 2级", "麻痹毒药 2级"}, + [40] = {"邪恶攻击 6级", "佯攻 3级", "潜行 3级", "安全降落", "致伤毒药 2级", "消失 2级"}, + [42] = {"切割 2级"}, + [44] = {"割裂 4级", "背刺 6级"}, + [46] = {"绞喉 5级", "破甲 4级", "致命毒药 3级"}, + [48] = {"刺骨 7级", "凿击 4级", "闷棍 3级", "致伤毒药 3级"}, + [50] = {"肾击 2级", "邪恶攻击 7级", "伏击 5级", "致残毒药 2级"}, + [52] = {"割裂 5级", "背刺 7级", "麻痹毒药 3级"}, + [54] = {"绞喉 6级", "邪恶攻击 8级", "致命毒药 4级"}, + [56] = {"刺骨 8级", "破甲 5级", "致伤毒药 4级"}, + [58] = {"脚踢 4级", "疾跑 3级"}, + [60] = {"割裂 6级", "凿击 5级", "佯攻 4级", "背刺 8级", "潜行 4级"}, + }, + PRIEST = { + [4] = {"暗言术:痛", "次级治疗术 2级"}, + [6] = {"真言术:盾", "惩击 2级"}, + [8] = {"恢复", "渐隐术"}, + [10] = {"暗言术:痛 2级", "心灵震爆", "复活术"}, + [12] = {"真言术:盾 2级", "心灵之火", "真言术:韧 2级", "祛病术"}, + [14] = {"恢复 2级", "心灵尖啸"}, + [16] = {"治疗术", "心灵震爆 2级"}, + [18] = {"真言术:盾 3级", "驱散魔法", "暗言术:痛 3级"}, + [20] = {"心灵之火 2级", "束缚亡灵", "快速治疗", "安抚心灵", "渐隐术 2级", "神圣之火"}, + [22] = {"惩击 4级", "心灵视界", "复活术 2级", "心灵震爆 3级"}, + [24] = {"真言术:盾 4级", "真言术:韧 3级", "法力燃烧", "神圣之火 2级"}, + [26] = {"恢复 4级", "暗言术:痛 4级"}, + [28] = {"治疗术 3级", "心灵震爆 4级", "心灵尖啸 2级"}, + [30] = {"真言术:盾 5级", "心灵之火 3级", "治疗祷言", "束缚亡灵 2级", "精神控制", "防护暗影", "渐隐术 3级"}, + [32] = {"法力燃烧 2级", "恢复 5级", "快速治疗 3级"}, + [34] = {"漂浮术", "暗言术:痛 5级", "心灵震爆 5级", "复活术 3级", "治疗术 4级"}, + [36] = {"真言术:盾 6级", "驱散魔法 2级", "真言术:韧 4级", "心灵之火 4级", "恢复 6级", "惩击 6级"}, + [38] = {"安抚心灵 2级"}, + [40] = {"法力燃烧 3级", "治疗祷言 2级", "防护暗影 2级", "心灵震爆 6级", "渐隐术 4级"}, + [42] = {"真言术:盾 7级", "神圣之火 5级", "心灵尖啸 3级"}, + [44] = {"恢复 7级", "精神控制 2级"}, + [46] = {"惩击 7级", "强效治疗术 2级", "心灵震爆 7级", "复活术 4级"}, + [48] = {"真言术:盾 8级", "真言术:韧 5级", "法力燃烧 4级", "神圣之火 6级", "恢复 8级", "暗言术:痛 7级"}, + [50] = {"心灵之火 5级", "治疗祷言 3级"}, + [52] = {"强效治疗术 3级", "心灵震爆 8级", "安抚心灵 3级"}, + [54] = {"真言术:盾 9级", "神圣之火 7级", "惩击 8级"}, + [56] = {"法力燃烧 5级", "恢复 9级", "防护暗影 3级", "心灵尖啸 4级", "暗言术:痛 8级"}, + [58] = {"复活术 5级", "强效治疗术 4级", "心灵震爆 9级"}, + [60] = {"真言术:盾 10级", "心灵之火 6级", "真言术:韧 6级", "束缚亡灵 3级", "治疗祷言 4级", "渐隐术 6级"}, + }, + SHAMAN = { + [4] = {"地震术"}, + [6] = {"治疗波 2级", "地缚图腾"}, + [8] = {"闪电箭 2级", "石爪图腾", "地震术 2级", "闪电之盾"}, + [10] = {"烈焰震击", "火舌武器", "大地之力图腾"}, + [12] = {"净化术", "火焰新星图腾", "先祖之魂", "治疗波 3级"}, + [14] = {"闪电箭 3级", "地震术 3级"}, + [16] = {"闪电之盾 2级", "消毒术"}, + [18] = {"烈焰震击 2级", "火舌武器 2级", "石爪图腾 2级", "治疗波 4级", "战栗图腾"}, + [20] = {"闪电箭 4级", "冰霜震击", "幽魂之狼", "次级治疗波"}, + [22] = {"火焰新星图腾 2级", "水下呼吸", "祛病术"}, + [24] = {"净化术 2级", "地震术 4级", "大地之力图腾 2级", "闪电之盾 3级", "先祖之魂 2级"}, + [26] = {"闪电箭 5级", "熔岩图腾", "火舌武器 3级", "视界术", "法力之泉图腾"}, + [28] = {"石爪图腾 3级", "烈焰震击 3级", "火舌图腾", "水上行走", "次级治疗波 2级"}, + [30] = {"星界传送", "根基图腾", "风怒武器", "治疗之泉图腾"}, + [32] = {"闪电箭 6级", "火焰新星图腾 3级", "闪电之盾 4级", "治疗波 6级", "闪电链", "风怒图腾"}, + [34] = {"冰霜震击 2级", "岗哨图腾"}, + [36] = {"地震术 5级", "熔岩图腾 2级", "火舌武器 4级", "法力之泉图腾 2级", "次级治疗波 3级", "风墙图腾"}, + [38] = {"石爪图腾 4级", "大地之力图腾 3级", "火舌图腾 2级"}, + [40] = {"闪电箭 8级", "闪电链 2级", "烈焰震击 4级", "治疗波 7级", "治疗链", "治疗之泉图腾 3级", "风怒武器 2级"}, + [42] = {"火焰新星图腾 4级"}, + [44] = {"闪电之盾 6级", "冰霜震击 3级", "熔岩图腾 3级", "风墙图腾 2级"}, + [46] = {"火舌武器 5级", "治疗链 2级"}, + [48] = {"地震术 6级", "石爪图腾 5级", "火舌图腾 3级", "治疗波 8级"}, + [50] = {"闪电箭 9级", "治疗之泉图腾 4级", "风怒武器 3级"}, + [52] = {"烈焰震击 5级", "大地之力图腾 4级", "次级治疗波 5级"}, + [54] = {"闪电箭 10级"}, + [56] = {"闪电链 4级", "熔岩图腾 4级", "火舌图腾 4级", "风墙图腾 3级", "治疗波 9级", "法力之泉图腾 4级"}, + [58] = {"冰霜震击 4级"}, + [60] = {"风怒武器 4级", "次级治疗波 6级", "治疗之泉图腾 5级"}, + }, + MAGE = { + [4] = {"造水术", "寒冰箭"}, + [6] = {"造食术", "火球术 2级", "火焰冲击"}, + [8] = {"变形术", "奥术飞弹"}, + [10] = {"霜甲术 2级", "冰霜新星"}, + [12] = {"缓落术", "造食术 2级", "火球术 3级"}, + [14] = {"魔爆术", "奥术智慧 2级", "火焰冲击 2级"}, + [16] = {"侦测魔法", "烈焰风暴"}, + [18] = {"解除次级诅咒", "魔法增效", "火球术 4级"}, + [20] = {"变形术 2级", "法力护盾", "闪现术", "霜甲术 3级", "暴风雪", "唤醒"}, + [22] = {"造食术 3级", "魔爆术 2级", "火焰冲击 3级", "灼烧"}, + [24] = {"火球术 5级", "烈焰风暴 2级", "法术反制"}, + [26] = {"寒冰箭 5级", "冰锥术"}, + [28] = {"奥术智慧 3级", "法力护盾 2级", "暴风雪 2级", "灼烧 2级", "冰霜新星 2级"}, + [30] = {"魔爆术 3级", "火球术 6级", "冰甲术"}, + [32] = {"造食术 4级", "烈焰风暴 3级", "寒冰箭 6级"}, + [34] = {"魔甲术", "冰锥术 2级", "灼烧 3级"}, + [36] = {"法力护盾 3级", "火球术 7级", "暴风雪 3级", "冰霜新星 3级"}, + [38] = {"魔爆术 4级", "寒冰箭 7级", "火焰冲击 5级"}, + [40] = {"造食术 5级", "奥术飞弹 5级", "火球术 8级", "冰甲术 2级", "灼烧 4级"}, + [42] = {"奥术智慧 4级"}, + [44] = {"法力护盾 4级", "暴风雪 4级", "寒冰箭 8级"}, + [46] = {"魔爆术 5级", "灼烧 5级"}, + [48] = {"火球术 9级", "奥术飞弹 6级", "烈焰风暴 5级"}, + [50] = {"造水术 6级", "寒冰箭 9级", "冰锥术 4级", "冰甲术 3级"}, + [52] = {"法力护盾 5级", "火球术 10级", "火焰冲击 7级", "冰霜新星 4级"}, + [54] = {"魔法增效 4级", "奥术飞弹 7级", "烈焰风暴 6级"}, + [56] = {"奥术智慧 5级", "寒冰箭 10级", "冰锥术 5级"}, + [58] = {"魔甲术 3级", "灼烧 7级"}, + [60] = {"变形术 4级", "法力护盾 6级", "火球术 11级", "暴风雪 6级", "冰甲术 4级"}, + }, + WARLOCK = { + [2] = {"痛苦诅咒", "恐惧术"}, + [4] = {"腐蚀术", "虚弱诅咒"}, + [6] = {"暗影箭 3级"}, + [8] = {"痛苦诅咒 2级"}, + [10] = {"吸取灵魂", "献祭 2级", "恶魔皮肤 2级", "制造初级治疗石"}, + [12] = {"生命分流", "生命通道", "魔息术"}, + [14] = {"腐蚀术 2级", "吸取生命", "鲁莽诅咒"}, + [16] = {"生命分流 2级"}, + [18] = {"痛苦诅咒 3级", "灼热之痛"}, + [20] = {"献祭 3级", "生命通道 2级", "暗影箭 4级", "魔甲术", "火焰之雨"}, + [22] = {"吸取生命 2级", "虚弱诅咒 3级", "基尔罗格之眼"}, + [24] = {"腐蚀术 3级", "吸取灵魂 2级", "吸取法力", "感知恶魔"}, + [26] = {"生命分流 3级", "语言诅咒"}, + [28] = {"鲁莽诅咒 2级", "痛苦诅咒 4级", "生命通道 3级", "放逐术"}, + [30] = {"吸取生命 3级", "献祭 4级", "奴役恶魔", "地狱烈焰", "魔甲术 2级"}, + [32] = {"虚弱诅咒 4级", "恐惧术 2级", "元素诅咒", "防护暗影结界"}, + [34] = {"生命分流 4级", "吸取法力 2级", "火焰之雨 2级", "灼热之痛 3级"}, + [36] = {"生命通道 4级"}, + [38] = {"吸取灵魂 3级", "痛苦诅咒 5级"}, + [40] = {"恐惧嚎叫", "献祭 5级", "奴役恶魔 2级"}, + [42] = {"虚弱诅咒 5级", "鲁莽诅咒 3级", "死亡缠绕", "防护暗影结界 2级", "地狱烈焰 2级", "灼热之痛 4级"}, + [44] = {"吸取生命 5级", "生命通道 5级", "暗影箭 7级"}, + [46] = {"生命分流 5级", "火焰之雨 3级"}, + [48] = {"痛苦诅咒 6级", "放逐术 2级", "灵魂之火"}, + [50] = {"虚弱诅咒 6级", "死亡缠绕 2级", "恐惧嚎叫 2级", "魔甲术 4级", "吸取灵魂 4级", "吸取法力 4级", "暗影箭 8级", "灼热之痛 5级"}, + [52] = {"防护暗影结界 3级", "生命通道 6级"}, + [54] = {"腐蚀术 6级", "吸取生命 6级", "地狱烈焰 3级", "灵魂之火 2级"}, + [56] = {"鲁莽诅咒 4级", "死亡缠绕 3级"}, + [58] = {"痛苦诅咒 7级", "奴役恶魔 3级", "火焰之雨 4级", "灼热之痛 6级"}, + [60] = {"厄运诅咒", "元素诅咒 3级", "魔甲术 5级", "暗影箭 9级"}, + }, + DRUID = { + [4] = {"月火术", "回春术"}, + [6] = {"荆棘术", "愤怒 2级"}, + [8] = {"纠缠根须", "治疗之触 2级"}, + [10] = {"月火术 2级", "回春术 2级", "挫志咆哮", "野性印记 2级"}, + [12] = {"愈合", "狂怒"}, + [14] = {"荆棘术 2级", "愤怒 3级", "重击"}, + [16] = {"月火术 3级", "回春术 3级", "挥击"}, + [18] = {"精灵之火", "休眠", "愈合 2级"}, + [20] = {"纠缠根须 2级", "星火术", "猎豹形态", "撕扯", "爪击", "治疗之触 4级", "潜行", "野性印记 3级", "复生"}, + [22] = {"愤怒 4级", "撕碎", "安抚动物"}, + [24] = {"荆棘术 3级", "挥击 2级", "扫击", "猛虎之怒", "解除诅咒"}, + [26] = {"星火术 2级", "月火术 5级", "爪击 2级", "治疗之触 5级", "驱毒术"}, + [28] = {"撕扯 2级", "挑战咆哮", "畏缩"}, + [30] = {"精灵之火 2级", "星火术 3级", "愤怒 5级", "旅行形态", "撕碎 2级", "重击 2级", "野性印记 4级", "宁静", "复生 2级"}, + [32] = {"挫志咆哮 3级", "挥击 3级", "毁灭", "治疗之触 6级", "凶猛撕咬"}, + [34] = {"荆棘术 4级", "月火术 6级", "回春术 6级", "扫击 2级", "爪击 3级"}, + [36] = {"愤怒 6级", "突袭", "狂暴回复"}, + [38] = {"纠缠根须 4级", "休眠 2级", "安抚动物 2级", "撕碎 3级"}, + [40] = {"星火术 4级", "飓风", "挥击 4级", "潜行 2级", "畏缩 2级", "巨熊形态", "凶猛撕咬 2级", "回春术 7级", "宁静 2级", "复生 3级", "激活"}, + [42] = {"挫志咆哮 4级", "毁灭 2级"}, + [44] = {"荆棘术 5级", "树皮术", "撕扯 4级", "扫击 3级", "治疗之触 8级"}, + [46] = {"愤怒 7级", "重击 3级", "突袭 2级"}, + [48] = {"纠缠根须 5级", "月火术 8级", "撕碎 4级"}, + [50] = {"星火术 5级", "宁静 3级", "复生 4级"}, + [52] = {"挫志咆哮 5级", "撕扯 5级", "畏缩 3级", "凶猛撕咬 4级", "回春术 9级"}, + [54] = {"荆棘术 6级", "愤怒 8级", "月火术 9级", "挥击 5级", "扫击 4级", "爪击 4级"}, + [56] = {"治疗之触 10级"}, + [58] = {"纠缠根须 6级", "星火术 6级", "月火术 10级", "爪击 5级", "毁灭 4级", "回春术 10级"}, + [60] = {"飓风 3级", "潜行 3级", "猛虎之怒 4级", "撕扯 6级", "宁静 4级", "复生 5级", "野性印记 7级", "愈合 9级"}, + }, +} + +SFrames.TalentTrainerSkills = { + WARRIOR = { + [48] = {{"致死打击 2级", "致死打击"}, {"嗜血 2级", "嗜血"}, {"盾牌猛击 2级", "盾牌猛击"}}, + [54] = {{"致死打击 3级", "致死打击"}, {"嗜血 3级", "嗜血"}, {"盾牌猛击 3级", "盾牌猛击"}}, + [60] = {{"致死打击 4级", "致死打击"}, {"嗜血 4级", "嗜血"}, {"盾牌猛击 4级", "盾牌猛击"}}, + }, + PALADIN = { + [48] = {{"神圣震击 2级", "神圣震击"}}, + [56] = {{"神圣震击 3级", "神圣震击"}}, + }, + HUNTER = { + [28] = {{"瞄准射击 2级", "瞄准射击"}}, + [36] = {{"瞄准射击 3级", "瞄准射击"}}, + [44] = {{"瞄准射击 4级", "瞄准射击"}}, + [52] = {{"瞄准射击 5级", "瞄准射击"}}, + [60] = {{"瞄准射击 6级", "瞄准射击"}}, + }, + ROGUE = { + [46] = {{"出血 2级", "出血"}}, + [58] = {{"出血 3级", "出血"}}, + }, + PRIEST = { + [28] = {{"精神鞭笞 2级", "精神鞭笞"}}, + [36] = {{"精神鞭笞 3级", "精神鞭笞"}}, + [44] = {{"精神鞭笞 4级", "精神鞭笞"}}, + [52] = {{"精神鞭笞 5级", "精神鞭笞"}}, + [60] = {{"精神鞭笞 6级", "精神鞭笞"}}, + }, + MAGE = { + [24] = {{"炎爆术 2级", "炎爆术"}}, + [30] = {{"炎爆术 3级", "炎爆术"}}, + [36] = {{"炎爆术 4级", "炎爆术"}}, + [42] = {{"炎爆术 5级", "炎爆术"}}, + [48] = {{"炎爆术 6级", "炎爆术"}, {"冲击波 2级", "冲击波"}, {"寒冰屏障 2级", "寒冰屏障"}}, + [54] = {{"炎爆术 7级", "炎爆术"}}, + [56] = {{"冲击波 3级", "冲击波"}, {"寒冰屏障 3级", "寒冰屏障"}}, + [60] = {{"炎爆术 8级", "炎爆术"}, {"冲击波 4级", "冲击波"}, {"寒冰屏障 4级", "寒冰屏障"}}, + }, + WARLOCK = { + [38] = {{"生命虹吸 2级", "生命虹吸"}}, + [48] = {{"生命虹吸 3级", "生命虹吸"}}, + [50] = {{"黑暗契约 2级", "黑暗契约"}}, + [58] = {{"生命虹吸 4级", "生命虹吸"}}, + [60] = {{"黑暗契约 3级", "黑暗契约"}}, + }, + DRUID = { + [30] = {{"虫群 2级", "虫群"}}, + [40] = {{"虫群 3级", "虫群"}}, + [50] = {{"虫群 4级", "虫群"}}, + [60] = {{"虫群 5级", "虫群"}}, + }, +} + +SFrames.ClassMountQuests = { + WARLOCK = { + [40] = "职业坐骑任务:召唤恶马", + [60] = "史诗坐骑任务:召唤恐惧战马", + }, + PALADIN = { + [40] = "职业坐骑任务:召唤战马", + [60] = "史诗坐骑任务:召唤战驹", + }, +} diff --git a/Config.lua b/Config.lua new file mode 100644 index 0000000..8364132 --- /dev/null +++ b/Config.lua @@ -0,0 +1,316 @@ +SFrames.Config = { + -- Default Settings + width = 220, + height = 50, + portraitWidth = 50, + castbarHeight = 18, + + colors = { + backdrop = { r = 0.15, g = 0.10, b = 0.15, a = 0.8 }, -- Pinkish dark tint + border = { r = 1.0, g = 0.5, b = 0.8, a = 1 }, -- Cute pink border + power = { + [0] = { r = 0.0, g = 0.0, b = 1.0 }, -- Mana + [1] = { r = 1.0, g = 0.0, b = 0.0 }, -- Rage + [2] = { r = 1.0, g = 0.5, b = 0.0 }, -- Focus + [3] = { r = 1.0, g = 1.0, b = 0.0 }, -- Energy + [4] = { r = 0.0, g = 1.0, b = 1.0 }, -- Happiness + }, + class = { + ["WARRIOR"] = { r = 0.78, g = 0.61, b = 0.43 }, + ["MAGE"] = { r = 0.41, g = 0.8, b = 0.94 }, + ["ROGUE"] = { r = 1.0, g = 0.96, b = 0.41 }, + ["DRUID"] = { r = 1.0, g = 0.49, b = 0.04 }, + ["HUNTER"] = { r = 0.67, g = 0.83, b = 0.45 }, + ["SHAMAN"] = { r = 0.14, g = 0.35, b = 1.0 }, + ["PRIEST"] = { r = 1.0, g = 1.0, b = 1.0 }, + ["WARLOCK"] = { r = 0.58, g = 0.51, b = 0.79 }, + ["PALADIN"] = { r = 0.96, g = 0.55, b = 0.73 }, + } + } +} + +-------------------------------------------------------------------------------- +-- Theme Engine +-------------------------------------------------------------------------------- + +SFrames.Theme = {} +SFrames.ActiveTheme = {} + +local function HSVtoRGB(h, s, v) + if s <= 0 then return v, v, v end + h = h - math.floor(h / 360) * 360 + local hh = h / 60 + local i = math.floor(hh) + local f = hh - i + local p = v * (1 - s) + local q = v * (1 - s * f) + local t = v * (1 - s * (1 - f)) + if i == 0 then return v, t, p + elseif i == 1 then return q, v, p + elseif i == 2 then return p, v, t + elseif i == 3 then return p, q, v + elseif i == 4 then return t, p, v + else return v, p, q end +end + +local function toHexChar(n) + if n < 10 then return string.char(48 + n) end + return string.char(97 + n - 10) +end + +local function RGBtoHex(r, g, b) + local rr = math.floor(r * 255 + 0.5) + local gg = math.floor(g * 255 + 0.5) + local bb = math.floor(b * 255 + 0.5) + return "ff" + .. toHexChar(math.floor(rr / 16)) .. toHexChar(rr - math.floor(rr / 16) * 16) + .. toHexChar(math.floor(gg / 16)) .. toHexChar(gg - math.floor(gg / 16) * 16) + .. toHexChar(math.floor(bb / 16)) .. toHexChar(bb - math.floor(bb / 16) * 16) +end + +SFrames.Theme.Presets = {} +SFrames.Theme.Presets["pink"] = { name = "樱粉", hue = 330, satMul = 1.00 } +SFrames.Theme.Presets["frost"] = { name = "霜蓝", hue = 210, satMul = 1.00 } +SFrames.Theme.Presets["emerald"] = { name = "翠绿", hue = 140, satMul = 0.85 } +SFrames.Theme.Presets["flame"] = { name = "炎橙", hue = 25, satMul = 0.90 } +SFrames.Theme.Presets["shadow"] = { name = "暗紫", hue = 270, satMul = 0.90 } +SFrames.Theme.Presets["golden"] = { name = "金辉", hue = 45, satMul = 0.80 } +SFrames.Theme.Presets["teal"] = { name = "碧青", hue = 175, satMul = 0.85 } +SFrames.Theme.Presets["crimson"] = { name = "绯红", hue = 5, satMul = 1.00 } +SFrames.Theme.Presets["holy"] = { name = "圣光", hue = 220, satMul = 0.15 } + +SFrames.Theme.PresetOrder = { "pink", "frost", "emerald", "flame", "shadow", "golden", "teal", "crimson", "holy" } + +SFrames.Theme.Presets["c_warrior"] = { name = "战士", hue = 31, satMul = 0.90, swatchRGB = {0.78, 0.61, 0.43} } +SFrames.Theme.Presets["c_paladin"] = { name = "圣骑士", hue = 334, satMul = 0.50, swatchRGB = {0.96, 0.55, 0.73} } +SFrames.Theme.Presets["c_hunter"] = { name = "猎人", hue = 85, satMul = 0.55, swatchRGB = {0.67, 0.83, 0.45} } +SFrames.Theme.Presets["c_rogue"] = { name = "潜行者", hue = 56, satMul = 0.70, swatchRGB = {1.00, 0.96, 0.41} } +SFrames.Theme.Presets["c_priest"] = { name = "牧师", hue = 40, satMul = 0.06, swatchRGB = {1.00, 1.00, 1.00} } +SFrames.Theme.Presets["c_shaman"] = { name = "萨满", hue = 225, satMul = 0.95, swatchRGB = {0.14, 0.35, 1.00} } +SFrames.Theme.Presets["c_mage"] = { name = "法师", hue = 196, satMul = 0.65, swatchRGB = {0.41, 0.80, 0.94} } +SFrames.Theme.Presets["c_warlock"] = { name = "术士", hue = 255, satMul = 0.45, swatchRGB = {0.58, 0.51, 0.79} } +SFrames.Theme.Presets["c_druid"] = { name = "德鲁伊", hue = 28, satMul = 1.00, swatchRGB = {1.00, 0.49, 0.04} } + +SFrames.Theme.ClassPresetOrder = { "c_warrior", "c_paladin", "c_hunter", "c_rogue", "c_priest", "c_shaman", "c_mage", "c_warlock", "c_druid" } + +SFrames.Theme.ClassMap = {} +SFrames.Theme.ClassMap["WARRIOR"] = "c_warrior" +SFrames.Theme.ClassMap["PALADIN"] = "c_paladin" +SFrames.Theme.ClassMap["HUNTER"] = "c_hunter" +SFrames.Theme.ClassMap["ROGUE"] = "c_rogue" +SFrames.Theme.ClassMap["PRIEST"] = "c_priest" +SFrames.Theme.ClassMap["SHAMAN"] = "c_shaman" +SFrames.Theme.ClassMap["MAGE"] = "c_mage" +SFrames.Theme.ClassMap["WARLOCK"] = "c_warlock" +SFrames.Theme.ClassMap["DRUID"] = "c_druid" + +local function GenerateTheme(H, satMul) + satMul = satMul or 1.0 + local function S(s) + local v = s * satMul + if v > 1 then v = 1 end + return v + end + local function C3(s, v) + local r, g, b = HSVtoRGB(H, S(s), v) + return { r, g, b } + end + local function C4(s, v, a) + local r, g, b = HSVtoRGB(H, S(s), v) + return { r, g, b, a } + end + local t = {} + t.accent = C4(0.40, 0.80, 0.98) + t.accentDark = C3(0.45, 0.55) + t.accentLight = C3(0.30, 1.00) + t.accentHex = RGBtoHex(t.accentLight[1], t.accentLight[2], t.accentLight[3]) + t.panelBg = C4(0.50, 0.12, 0.95) + t.panelBorder = C4(0.45, 0.55, 0.90) + t.headerBg = C4(0.60, 0.10, 0.98) + t.sectionBg = C4(0.43, 0.14, 0.82) + t.sectionBorder = C4(0.38, 0.45, 0.86) + t.bg = t.panelBg + t.border = t.panelBorder + t.slotBg = C4(0.20, 0.07, 0.90) + t.slotBorder = C4(0.10, 0.28, 0.80) + t.slotHover = C4(0.38, 0.40, 0.90) + t.slotSelected = C4(0.43, 0.70, 1.00) + t.buttonBg = C4(0.44, 0.18, 0.94) + t.buttonBorder = C4(0.40, 0.50, 0.90) + t.buttonHoverBg = C4(0.47, 0.30, 0.96) + t.buttonDownBg = C4(0.50, 0.14, 0.96) + t.buttonDisabledBg = C4(0.43, 0.14, 0.65) + t.buttonActiveBg = C4(0.52, 0.42, 0.98) + t.buttonActiveBorder = C4(0.42, 0.90, 1.00) + t.buttonText = C3(0.16, 0.90) + t.buttonActiveText = C3(0.08, 1.00) + t.buttonDisabledText = C4(0.14, 0.55, 0.68) + t.btnBg = t.buttonBg + t.btnBorder = t.buttonBorder + t.btnHoverBg = t.buttonHoverBg + t.btnHoverBd = C4(0.40, 0.80, 0.98) + t.btnDownBg = t.buttonDownBg + t.btnText = t.buttonText + t.btnActiveText = t.buttonActiveText + t.btnDisabledText = C3(0.14, 0.40) + t.btnHover = C4(0.47, 0.30, 0.95) + t.btnHoverBorder = t.btnHoverBd + t.tabBg = t.buttonBg + t.tabBorder = t.buttonBorder + t.tabActiveBg = C4(0.50, 0.32, 0.96) + t.tabActiveBorder = C4(0.40, 0.80, 0.98) + t.tabText = C3(0.21, 0.70) + t.tabActiveText = t.buttonActiveText + t.checkBg = t.buttonBg + t.checkBorder = t.buttonBorder + t.checkHoverBorder = C4(0.40, 0.80, 0.95) + t.checkFill = C4(0.43, 0.88, 0.98) + t.checkOn = C3(0.40, 0.80) + t.checkOff = C4(0.40, 0.25, 0.60) + t.sliderTrack = C4(0.45, 0.22, 0.90) + t.sliderFill = C4(0.35, 0.85, 0.92) + t.sliderThumb = C4(0.25, 1.00, 0.95) + t.text = C3(0.11, 0.92) + t.title = C3(0.30, 1.00) + t.gold = t.title + t.nameText = C3(0.06, 0.92) + t.dimText = C3(0.25, 0.60) + t.bodyText = C3(0.05, 0.82) + t.sectionTitle = C3(0.24, 0.90) + t.catHeader = C3(0.31, 0.80) + t.colHeader = C3(0.25, 0.80) + t.labelText = C3(0.23, 0.65) + t.valueText = t.text + t.subText = t.labelText + t.pageText = C3(0.19, 0.80) + t.objectiveText = C3(0.10, 0.90) + t.optionText = t.tabText + t.countText = t.tabText + t.trackText = C3(0.25, 0.80) + t.divider = C4(0.45, 0.55, 0.40) + t.sepColor = C4(0.44, 0.45, 0.50) + t.scrollThumb = C4(0.45, 0.55, 0.70) + t.scrollTrack = C4(0.50, 0.08, 0.50) + t.inputBg = C4(0.50, 0.08, 0.95) + t.inputBorder = C4(0.38, 0.40, 0.80) + t.searchBg = C4(0.50, 0.08, 0.80) + t.searchBorder = C4(0.38, 0.40, 0.60) + t.progressBg = C4(0.50, 0.08, 0.90) + t.progressFill = C4(0.50, 0.70, 1.00) + t.modelBg = C4(0.60, 0.08, 0.85) + t.modelBorder = C4(0.43, 0.35, 0.70) + t.emptySlot = C4(0.40, 0.25, 0.40) + t.emptySlotBg = C4(0.50, 0.08, 0.40) + t.emptySlotBd = C4(0.40, 0.25, 0.30) + t.barBg = C4(0.60, 0.10, 1.00) + t.rowNormal = C4(0.50, 0.06, 0.30) + t.rowNormalBd = C4(0.22, 0.20, 0.30) + t.raidGroup = t.sectionBg + t.raidGroupBorder = C4(0.38, 0.40, 0.70) + t.raidSlotEmpty = C4(0.50, 0.08, 0.60) + t.questSelected = C4(0.70, 0.60, 0.85) + t.questSelBorder = C4(0.47, 0.95, 1.00) + t.questSelBar = C4(0.45, 1.00, 1.00) + t.questHover = C4(0.52, 0.25, 0.50) + t.zoneHeader = t.catHeader + t.zoneBg = C4(0.50, 0.14, 0.50) + t.collapseIcon = C3(0.31, 0.70) + t.trackBar = C4(0.53, 0.95, 1.00) + t.trackGlow = C4(0.53, 0.95, 0.22) + t.rewardBg = C4(0.50, 0.10, 0.85) + t.rewardBorder = C4(0.45, 0.40, 0.70) + t.listBg = C4(0.50, 0.08, 0.80) + t.listBorder = C4(0.43, 0.35, 0.60) + t.detailBg = C4(0.50, 0.09, 0.92) + t.detailBorder = t.listBorder + t.selectedRowBg = C4(0.65, 0.35, 0.60) + t.selectedRowBorder = C4(0.50, 0.90, 0.70) + t.selectedNameText = { 1, 0.95, 1 } + t.overlayBg = C4(0.75, 0.04, 0.55) + t.accentLine = C4(0.50, 1.00, 0.90) + t.titleColor = t.title + t.nameColor = { 1, 1, 1 } + t.valueColor = t.text + t.labelColor = C3(0.28, 0.58) + t.dimColor = C3(0.29, 0.48) + t.clockColor = C3(0.18, 1.00) + t.timerColor = C3(0.27, 0.75) + t.brandColor = C4(0.37, 0.60, 0.70) + t.particleColor = C3(0.40, 1.00) + t.wbGold = { 1, 0.88, 0.55 } + t.wbBorder = { 0.95, 0.75, 0.25 } + t.passive = { 0.60, 0.60, 0.65 } + return t +end + +function SFrames.Theme:Extend(extras) + local override = extras or {} + local proxy = {} + setmetatable(proxy, { + __index = function(self, k) + local v = override[k] + if v ~= nil then return v end + return SFrames.ActiveTheme[k] + end + }) + return proxy +end + +function SFrames.Theme:Apply(presetKey) + local key = presetKey or "pink" + local preset = self.Presets[key] + if not preset then key = "pink"; preset = self.Presets["pink"] end + local newTheme = GenerateTheme(preset.hue, preset.satMul) + local oldKeys = {} + for k, v in pairs(SFrames.ActiveTheme) do + table.insert(oldKeys, k) + end + for i = 1, table.getn(oldKeys) do + SFrames.ActiveTheme[oldKeys[i]] = nil + end + for k, v in pairs(newTheme) do + SFrames.ActiveTheme[k] = v + end + if SFrames.Config and SFrames.Config.colors then + local a = SFrames.ActiveTheme + SFrames.Config.colors.border = { r = a.accent[1], g = a.accent[2], b = a.accent[3], a = 1 } + SFrames.Config.colors.backdrop = { r = a.panelBg[1], g = a.panelBg[2], b = a.panelBg[3], a = a.panelBg[4] or 0.8 } + end + if SFrames.MinimapButton and SFrames.MinimapButton.Refresh then + SFrames.MinimapButton:Refresh() + end +end + +function SFrames.Theme:GetCurrentPreset() + local dbExists = SFramesDB and true or false + local themeExists = SFramesDB and type(SFramesDB.Theme) == "table" and true or false + local savedPreset = themeExists and SFramesDB.Theme.preset or "nil" + if SFramesDB and type(SFramesDB.Theme) == "table" then + if SFramesDB.Theme.useClassTheme then + local _, class = UnitClass("player") + if class and self.ClassMap[class] then + return self.ClassMap[class] + end + end + if SFramesDB.Theme.preset and self.Presets[SFramesDB.Theme.preset] then + return SFramesDB.Theme.preset + end + end + return "pink" +end + +function SFrames.Theme:GetAccentHex() + return SFrames.ActiveTheme.accentHex or "ffffb3d9" +end + +SFrames.Theme.HSVtoRGB = HSVtoRGB +SFrames.Theme.RGBtoHex = RGBtoHex + +SFrames.Theme:Apply(SFrames.Theme:GetCurrentPreset()) + +local themeInitFrame = CreateFrame("Frame") +themeInitFrame:RegisterEvent("VARIABLES_LOADED") +themeInitFrame:RegisterEvent("PLAYER_LOGIN") +themeInitFrame:SetScript("OnEvent", function() + SFrames.Theme:Apply(SFrames.Theme:GetCurrentPreset()) +end) diff --git a/ConfigUI.lua b/ConfigUI.lua new file mode 100644 index 0000000..aa3283a --- /dev/null +++ b/ConfigUI.lua @@ -0,0 +1,3513 @@ +-------------------------------------------------------------------------------- +-- S-Frames: In-Game Settings Panel (ConfigUI.lua) +-- Pages: +-- 1) UI Settings (scrollable) +-- 2) Bag Settings +-------------------------------------------------------------------------------- + +SFrames.ConfigUI = SFrames.ConfigUI or {} +SFrames.ConfigUI.ready = true + +local PANEL_WIDTH = 700 +local PANEL_HEIGHT = 560 + +local BAG_DEFAULTS = { + enable = true, + columns = 10, + bagSpacing = 0, + scale = 1, + sellGrey = true, + bankColumns = 12, + bankSpacing = 0, + bankScale = 1, +} + +local widgetId = 0 + +local function NextWidgetName(prefix) + widgetId = widgetId + 1 + return "SFramesConfig" .. prefix .. tostring(widgetId) +end + +local function Clamp(value, minValue, maxValue) + if value < minValue then return minValue end + if value > maxValue then return maxValue end + return value +end + +local SOFT_THEME = SFrames.ActiveTheme + +local function EnsureSoftBackdrop(frame) + if not frame then return end + if frame.sfSoftBackdrop then return end + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + frame.sfSoftBackdrop = true +end + +-- Thin 1px border backdrop, suitable for small frames like checkbox boxes +local function EnsureThinBackdrop(frame) + if not frame then return end + if frame.sfSoftBackdrop then return end + if SFrames and SFrames.CreateBackdrop then + SFrames:CreateBackdrop(frame) + elseif frame.SetBackdrop then + frame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + end + frame.sfSoftBackdrop = true +end + +local function HideTexture(tex) + if not tex then return end + if tex.SetTexture then tex:SetTexture(nil) end + tex:Hide() +end + +local function StyleButton(btn) + if not btn or btn.sfSoftStyled then return btn end + btn.sfSoftStyled = true + + -- Apply ESC menu style round backdrop + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + HideTexture(btn.GetNormalTexture and btn:GetNormalTexture()) + HideTexture(btn.GetPushedTexture and btn:GetPushedTexture()) + HideTexture(btn.GetHighlightTexture and btn:GetHighlightTexture()) + HideTexture(btn.GetDisabledTexture and btn:GetDisabledTexture()) + + local name = btn:GetName() or "" + for _, suffix in ipairs({ "Left", "Right", "Middle" }) do + local tex = _G[name .. suffix] + if tex then tex:SetAlpha(0) tex:Hide() end + end + + local function SetVisual(state) + local bg = SOFT_THEME.buttonBg + local border = SOFT_THEME.buttonBorder + local text = SOFT_THEME.buttonText + + if btn.sfSoftActive then + bg = SOFT_THEME.buttonActiveBg + border = SOFT_THEME.buttonActiveBorder + text = SOFT_THEME.buttonActiveText + elseif state == "hover" then + bg = SOFT_THEME.buttonHoverBg + border = (SFrames.ActiveTheme and SFrames.ActiveTheme.btnHoverBd) or { 0.80, 0.48, 0.64, 0.98 } + text = SOFT_THEME.buttonActiveText + elseif state == "down" then + bg = SOFT_THEME.buttonDownBg + elseif state == "disabled" then + bg = SOFT_THEME.buttonDisabledBg + text = SOFT_THEME.buttonDisabledText + end + + if btn.SetBackdropColor then + btn:SetBackdropColor(bg[1], bg[2], bg[3], bg[4]) + end + if btn.SetBackdropBorderColor then + btn:SetBackdropBorderColor(border[1], border[2], border[3], border[4]) + end + local fs = btn.GetFontString and btn:GetFontString() + if fs then + fs:SetTextColor(text[1], text[2], text[3]) + end + end + + btn.RefreshVisual = function() + if btn.sfSoftActive then + SetVisual("active") + elseif btn.IsEnabled and not btn:IsEnabled() then + SetVisual("disabled") + else + SetVisual("normal") + end + end + + local oldEnter = btn:GetScript("OnEnter") + local oldLeave = btn:GetScript("OnLeave") + local oldDown = btn:GetScript("OnMouseDown") + local oldUp = btn:GetScript("OnMouseUp") + + btn:SetScript("OnEnter", function() + if oldEnter then oldEnter() end + if this.IsEnabled and this:IsEnabled() and not this.sfSoftActive then + SetVisual("hover") + end + end) + btn:SetScript("OnLeave", function() + if oldLeave then oldLeave() end + if this.IsEnabled and this:IsEnabled() and not this.sfSoftActive then + SetVisual("normal") + end + end) + btn:SetScript("OnMouseDown", function() + if oldDown then oldDown() end + if this.IsEnabled and this:IsEnabled() and not this.sfSoftActive then + SetVisual("down") + end + end) + btn:SetScript("OnMouseUp", function() + if oldUp then oldUp() end + if this.IsEnabled and this:IsEnabled() and not this.sfSoftActive then + SetVisual("hover") + end + end) + + btn:RefreshVisual() + return btn +end + +local function AddBtnIcon(btn, iconKey, size, align) + if not (SFrames and SFrames.CreateIcon) then return end + local sz = size or 12 + local gap = 3 + local ico = SFrames:CreateIcon(btn, iconKey, sz) + ico:SetDrawLayer("OVERLAY") + btn.nanamiIcon = ico + local fs = btn.GetFontString and btn:GetFontString() + if fs then + fs:ClearAllPoints() + if align == "left" then + ico:SetPoint("LEFT", btn, "LEFT", 8, 0) + fs:SetPoint("LEFT", ico, "RIGHT", gap, 0) + fs:SetPoint("RIGHT", btn, "RIGHT", -4, 0) + fs:SetJustifyH("LEFT") + else + fs:SetPoint("CENTER", btn, "CENTER", (sz + gap) / 2, 0) + ico:SetPoint("RIGHT", fs, "LEFT", -gap, 0) + end + else + ico:SetPoint("CENTER", btn, "CENTER", 0, 0) + end +end + +local function StyleCheckBox(cb) + if not cb or cb.sfSoftStyled then return cb end + cb.sfSoftStyled = true + + -- Hide ALL default CheckButton textures including the checked texture + HideTexture(cb.GetNormalTexture and cb:GetNormalTexture()) + HideTexture(cb.GetPushedTexture and cb:GetPushedTexture()) + HideTexture(cb.GetHighlightTexture and cb:GetHighlightTexture()) + HideTexture(cb.GetDisabledTexture and cb:GetDisabledTexture()) + if cb.SetCheckedTexture then cb:SetCheckedTexture("") end + if cb.SetDisabledCheckedTexture then cb:SetDisabledCheckedTexture("") end + + -- Round-corner box (tooltip border = soft rounded edges) + local box = CreateFrame("Frame", nil, cb) + box:SetPoint("TOPLEFT", cb, "TOPLEFT", 0, 0) + box:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 0, 0) + box:SetFrameLevel(cb:GetFrameLevel() + 1) + box:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + cb.sfBox = box + + local function ApplyState(isChecked) + if isChecked then + box:SetBackdropColor( + SOFT_THEME.checkFill[1], SOFT_THEME.checkFill[2], + SOFT_THEME.checkFill[3], 0.88) + box:SetBackdropBorderColor( + SOFT_THEME.checkFill[1], SOFT_THEME.checkFill[2], + SOFT_THEME.checkFill[3], 1) + else + box:SetBackdropColor( + SOFT_THEME.checkBg[1], SOFT_THEME.checkBg[2], + SOFT_THEME.checkBg[3], SOFT_THEME.checkBg[4]) + box:SetBackdropBorderColor( + SOFT_THEME.checkBorder[1], SOFT_THEME.checkBorder[2], + SOFT_THEME.checkBorder[3], SOFT_THEME.checkBorder[4]) + end + end + cb.sfApplyState = ApplyState + + -- Text label: push it away from the box with a left margin + local label = _G[cb:GetName() .. "Text"] + if label then + label:ClearAllPoints() + label:SetPoint("LEFT", cb, "RIGHT", 4, 0) + label:SetFont(SFrames:GetFont(), 11, "OUTLINE") + label:SetJustifyH("LEFT") + label:SetTextColor(SOFT_THEME.text[1], SOFT_THEME.text[2], SOFT_THEME.text[3]) + end + + -- Hover: brighten border when unchecked + cb:SetScript("OnEnter", function() + if not this.sfChecked then + if this.sfBox then + this.sfBox:SetBackdropBorderColor( + SOFT_THEME.checkHoverBorder[1], SOFT_THEME.checkHoverBorder[2], + SOFT_THEME.checkHoverBorder[3], SOFT_THEME.checkHoverBorder[4]) + end + end + end) + cb:SetScript("OnLeave", function() + if not this.sfChecked then + if this.sfBox then + this.sfBox:SetBackdropBorderColor( + SOFT_THEME.checkBorder[1], SOFT_THEME.checkBorder[2], + SOFT_THEME.checkBorder[3], SOFT_THEME.checkBorder[4]) + end + end + end) + + ApplyState(false) + return cb +end + +local function StyleSlider(slider, low, high, text) + if not slider or slider.sfSoftStyled then return slider end + slider.sfSoftStyled = true + + local regions = { slider:GetRegions() } + for i = 1, table.getn(regions) do + local region = regions[i] + if region and region.GetObjectType and region:GetObjectType() == "Texture" then + region:SetTexture(nil) + end + end + + local track = slider:CreateTexture(nil, "BACKGROUND") + track:SetTexture("Interface\\Buttons\\WHITE8X8") + track:SetPoint("LEFT", slider, "LEFT", 0, 0) + track:SetPoint("RIGHT", slider, "RIGHT", 0, 0) + track:SetHeight(4) + track:SetVertexColor(SOFT_THEME.sliderTrack[1], SOFT_THEME.sliderTrack[2], SOFT_THEME.sliderTrack[3], SOFT_THEME.sliderTrack[4]) + slider.sfTrack = track + + local fill = slider:CreateTexture(nil, "ARTWORK") + fill:SetTexture("Interface\\Buttons\\WHITE8X8") + fill:SetPoint("LEFT", track, "LEFT", 0, 0) + fill:SetPoint("TOP", track, "TOP", 0, 0) + fill:SetPoint("BOTTOM", track, "BOTTOM", 0, 0) + fill:SetWidth(1) + fill:SetVertexColor(SOFT_THEME.sliderFill[1], SOFT_THEME.sliderFill[2], SOFT_THEME.sliderFill[3], SOFT_THEME.sliderFill[4]) + slider.sfFill = fill + + if slider.SetThumbTexture then + slider:SetThumbTexture("Interface\\Buttons\\WHITE8X8") + end + local thumb = slider.GetThumbTexture and slider:GetThumbTexture() + if thumb then + thumb:SetWidth(8) + thumb:SetHeight(16) + thumb:SetVertexColor(SOFT_THEME.sliderThumb[1], SOFT_THEME.sliderThumb[2], SOFT_THEME.sliderThumb[3], SOFT_THEME.sliderThumb[4]) + end + + local function UpdateFill() + local minValue, maxValue = slider:GetMinMaxValues() + local value = slider:GetValue() or minValue + local pct = 0 + if maxValue > minValue then + pct = (value - minValue) / (maxValue - minValue) + end + pct = Clamp(pct, 0, 1) + local width = math.floor((slider:GetWidth() or 1) * pct + 0.5) + if width < 1 then width = 1 end + slider.sfFill:SetWidth(width) + end + + local oldChanged = slider:GetScript("OnValueChanged") + slider:SetScript("OnValueChanged", function() + if oldChanged then oldChanged() end + UpdateFill() + end) + UpdateFill() + + if low then + low:SetTextColor(SOFT_THEME.dimText[1], SOFT_THEME.dimText[2], SOFT_THEME.dimText[3]) + low:ClearAllPoints() + low:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 0, 0) + end + if high then + high:SetTextColor(SOFT_THEME.dimText[1], SOFT_THEME.dimText[2], SOFT_THEME.dimText[3]) + high:ClearAllPoints() + high:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", 0, 0) + end + if text then + text:SetTextColor(SOFT_THEME.text[1], SOFT_THEME.text[2], SOFT_THEME.text[3]) + text:ClearAllPoints() + text:SetPoint("BOTTOM", slider, "TOP", 0, 2) + end + return slider +end + +local function StyleScrollBar(slider, sliderName) + if not slider or slider.sfSoftStyled then return slider end + slider.sfSoftStyled = true + + slider:SetWidth(12) + local regions = { slider:GetRegions() } + for i = 1, table.getn(regions) do + local region = regions[i] + if region and region.GetObjectType and region:GetObjectType() == "Texture" then + region:SetTexture(nil) + end + end + + local track = slider:CreateTexture(nil, "BACKGROUND") + track:SetTexture("Interface\\Buttons\\WHITE8X8") + track:SetPoint("TOPLEFT", slider, "TOPLEFT", 3, 0) + track:SetPoint("BOTTOMRIGHT", slider, "BOTTOMRIGHT", -3, 0) + track:SetVertexColor(0.18, 0.19, 0.22, 0.9) + + if slider.SetThumbTexture then + slider:SetThumbTexture("Interface\\Buttons\\WHITE8X8") + end + local thumb = slider.GetThumbTexture and slider:GetThumbTexture() + if thumb then + thumb:SetWidth(8) + thumb:SetHeight(20) + thumb:SetVertexColor(SOFT_THEME.scrollThumb[1], SOFT_THEME.scrollThumb[2], SOFT_THEME.scrollThumb[3], SOFT_THEME.scrollThumb[4] or 0.95) + end + + local upBtn = _G[sliderName .. "ScrollUpButton"] + local downBtn = _G[sliderName .. "ScrollDownButton"] + if upBtn then upBtn:Hide() end + if downBtn then downBtn:Hide() end + return slider +end + +local function EnsureDB() + if not SFramesDB then SFramesDB = {} end + + if SFramesDB.showLevel == nil then SFramesDB.showLevel = true end + if SFramesDB.classColorHealth == nil then SFramesDB.classColorHealth = true end + if SFramesDB.enableMerchant == nil then SFramesDB.enableMerchant = true end + if SFramesDB.enableQuestUI == nil then SFramesDB.enableQuestUI = true end + if SFramesDB.enableQuestLogSkin == nil then SFramesDB.enableQuestLogSkin = true end + if SFramesDB.enableTrainer == nil then SFramesDB.enableTrainer = true end + if SFramesDB.enableSpellBook == nil then SFramesDB.enableSpellBook = true end + if SFramesDB.enableTradeSkill == nil then SFramesDB.enableTradeSkill = true end + if SFramesDB.spellBookHighestOnly == nil then SFramesDB.spellBookHighestOnly = false end + if SFramesDB.spellBookAutoReplace == nil then SFramesDB.spellBookAutoReplace = false end + if SFramesDB.enableSocial == nil then SFramesDB.enableSocial = true end + if SFramesDB.enableInspect == nil then SFramesDB.enableInspect = true end + if SFramesDB.enableFlightMap == nil then SFramesDB.enableFlightMap = true end + if SFramesDB.enablePetStable == nil then SFramesDB.enablePetStable = true end + if SFramesDB.enableMail == nil then SFramesDB.enableMail = true end + if type(SFramesDB.spellBookScale) ~= "number" then SFramesDB.spellBookScale = 1 end + if type(SFramesDB.socialScale) ~= "number" then SFramesDB.socialScale = 1 end + + if type(SFramesDB.playerFrameScale) ~= "number" then SFramesDB.playerFrameScale = 1 end + if type(SFramesDB.playerFrameWidth) ~= "number" then SFramesDB.playerFrameWidth = 220 end + if type(SFramesDB.playerPortraitWidth) ~= "number" then SFramesDB.playerPortraitWidth = 50 end + if type(SFramesDB.playerHealthHeight) ~= "number" then SFramesDB.playerHealthHeight = 38 end + if type(SFramesDB.playerPowerHeight) ~= "number" then SFramesDB.playerPowerHeight = 9 end + if SFramesDB.playerShowClass == nil then SFramesDB.playerShowClass = true end + if SFramesDB.playerShowClassIcon == nil then SFramesDB.playerShowClassIcon = true end + if type(SFramesDB.playerNameFontSize) ~= "number" then SFramesDB.playerNameFontSize = 10 end + if type(SFramesDB.playerValueFontSize) ~= "number" then SFramesDB.playerValueFontSize = 10 end + + if type(SFramesDB.targetFrameScale) ~= "number" then SFramesDB.targetFrameScale = 1 end + if type(SFramesDB.targetFrameWidth) ~= "number" then SFramesDB.targetFrameWidth = 220 end + if type(SFramesDB.targetPortraitWidth) ~= "number" then SFramesDB.targetPortraitWidth = 50 end + if type(SFramesDB.targetHealthHeight) ~= "number" then SFramesDB.targetHealthHeight = 38 end + if type(SFramesDB.targetPowerHeight) ~= "number" then SFramesDB.targetPowerHeight = 9 end + if SFramesDB.targetShowClass == nil then SFramesDB.targetShowClass = true end + if SFramesDB.targetShowClassIcon == nil then SFramesDB.targetShowClassIcon = true end + if type(SFramesDB.targetNameFontSize) ~= "number" then SFramesDB.targetNameFontSize = 10 end + if type(SFramesDB.targetValueFontSize) ~= "number" then SFramesDB.targetValueFontSize = 10 end + if SFramesDB.targetDistanceEnabled == nil then SFramesDB.targetDistanceEnabled = true end + if type(SFramesDB.targetDistanceScale) ~= "number" then SFramesDB.targetDistanceScale = 1 end + + if SFramesDB.showPetFrame == nil then SFramesDB.showPetFrame = true end + if type(SFramesDB.petFrameScale) ~= "number" then SFramesDB.petFrameScale = 1 end + + if SFramesDB.partyLayout ~= "horizontal" and SFramesDB.partyLayout ~= "vertical" then + SFramesDB.partyLayout = "vertical" + end + if type(SFramesDB.partyFrameScale) ~= "number" then SFramesDB.partyFrameScale = 1 end + if type(SFramesDB.partyFrameWidth) ~= "number" then SFramesDB.partyFrameWidth = 150 end + if type(SFramesDB.partyFrameHeight) ~= "number" then SFramesDB.partyFrameHeight = 35 end + if type(SFramesDB.partyPortraitWidth) ~= "number" then SFramesDB.partyPortraitWidth = 33 end + if type(SFramesDB.partyHealthHeight) ~= "number" then SFramesDB.partyHealthHeight = 22 end + if type(SFramesDB.partyPowerHeight) ~= "number" then SFramesDB.partyPowerHeight = 10 end + if type(SFramesDB.partyHorizontalGap) ~= "number" then SFramesDB.partyHorizontalGap = 8 end + if type(SFramesDB.partyVerticalGap) ~= "number" then SFramesDB.partyVerticalGap = 30 end + if type(SFramesDB.partyNameFontSize) ~= "number" then SFramesDB.partyNameFontSize = 10 end + if type(SFramesDB.partyValueFontSize) ~= "number" then SFramesDB.partyValueFontSize = 10 end + if SFramesDB.partyShowBuffs == nil then SFramesDB.partyShowBuffs = true end + if SFramesDB.partyShowDebuffs == nil then SFramesDB.partyShowDebuffs = true end + + if SFramesDB.raidLayout ~= "horizontal" and SFramesDB.raidLayout ~= "vertical" then + SFramesDB.raidLayout = "horizontal" + end + if type(SFramesDB.raidFrameScale) ~= "number" then SFramesDB.raidFrameScale = 1 end + if type(SFramesDB.raidFrameWidth) ~= "number" then SFramesDB.raidFrameWidth = 60 end + if type(SFramesDB.raidFrameHeight) ~= "number" then SFramesDB.raidFrameHeight = 40 end + if type(SFramesDB.raidHealthHeight) ~= "number" then SFramesDB.raidHealthHeight = 31 end + if type(SFramesDB.raidHorizontalGap) ~= "number" then SFramesDB.raidHorizontalGap = 2 end + if type(SFramesDB.raidVerticalGap) ~= "number" then SFramesDB.raidVerticalGap = 2 end + if type(SFramesDB.raidGroupGap) ~= "number" then SFramesDB.raidGroupGap = 8 end + if type(SFramesDB.raidNameFontSize) ~= "number" then SFramesDB.raidNameFontSize = 10 end + if type(SFramesDB.raidValueFontSize) ~= "number" then SFramesDB.raidValueFontSize = 9 end + if SFramesDB.raidShowPower == nil then SFramesDB.raidShowPower = true end + if SFramesDB.enableRaidFrames == nil then SFramesDB.enableRaidFrames = true end + if SFramesDB.raidHealthFormat == nil then SFramesDB.raidHealthFormat = "compact" end + if SFramesDB.raidShowGroupLabel == nil then SFramesDB.raidShowGroupLabel = true end + + if SFramesDB.charPanelEnable == nil then SFramesDB.charPanelEnable = true end + + if SFramesDB.afkEnabled == nil then SFramesDB.afkEnabled = true end + if type(SFramesDB.afkDelay) ~= "number" then SFramesDB.afkDelay = 5 end + if SFramesDB.afkOutsideRest == nil then SFramesDB.afkOutsideRest = false end + + if SFramesDB.trainerReminder == nil then SFramesDB.trainerReminder = true end + + if SFramesDB.smoothBars == nil then SFramesDB.smoothBars = true end + if SFramesDB.mobRealHealth == nil then SFramesDB.mobRealHealth = true end + if SFramesDB.showItemLevel == nil then SFramesDB.showItemLevel = true end + if SFramesDB.itemCompare == nil then SFramesDB.itemCompare = true end + if SFramesDB.gearScore == nil then SFramesDB.gearScore = true end + + if type(SFramesDB.Bags) ~= "table" then SFramesDB.Bags = {} end + local defaults = BAG_DEFAULTS + if SFrames and SFrames.Config and type(SFrames.Config.Bags) == "table" then defaults = SFrames.Config.Bags end + for key, value in pairs(defaults) do + if SFramesDB.Bags[key] == nil then SFramesDB.Bags[key] = value end + end + + if type(SFramesDB.Minimap) ~= "table" then SFramesDB.Minimap = {} end + if SFramesDB.Minimap.enabled == nil then SFramesDB.Minimap.enabled = true end + if type(SFramesDB.Minimap.scale) ~= "number" then SFramesDB.Minimap.scale = 1.0 end + if SFramesDB.Minimap.showClock == nil then SFramesDB.Minimap.showClock = true end + if SFramesDB.Minimap.showCoords == nil then SFramesDB.Minimap.showCoords = true end + if SFramesDB.Minimap.mapStyle == nil then SFramesDB.Minimap.mapStyle = "auto" end + if type(SFramesDB.Minimap.posX) ~= "number" then SFramesDB.Minimap.posX = -5 end + if type(SFramesDB.Minimap.posY) ~= "number" then SFramesDB.Minimap.posY = -5 end + + if type(SFramesDB.MapReveal) ~= "table" then SFramesDB.MapReveal = {} end + if SFramesDB.MapReveal.enabled == nil then SFramesDB.MapReveal.enabled = true end + if type(SFramesDB.MapReveal.unexploredAlpha) ~= "number" then SFramesDB.MapReveal.unexploredAlpha = 0.7 end + + if type(SFramesDB.Tweaks) ~= "table" then SFramesDB.Tweaks = {} end + if SFramesDB.Tweaks.autoStance == nil then SFramesDB.Tweaks.autoStance = true end + if SFramesDB.Tweaks.autoDismount == nil then SFramesDB.Tweaks.autoDismount = true end + if SFramesDB.Tweaks.superWoW == nil then SFramesDB.Tweaks.superWoW = true end + if SFramesDB.Tweaks.turtleCompat == nil then SFramesDB.Tweaks.turtleCompat = true end + if SFramesDB.Tweaks.cooldownNumbers == nil then SFramesDB.Tweaks.cooldownNumbers = true end + if SFramesDB.Tweaks.darkUI == nil then SFramesDB.Tweaks.darkUI = false end + if SFramesDB.Tweaks.worldMapWindow == nil then SFramesDB.Tweaks.worldMapWindow = false end + + if type(SFramesDB.WorldMap) ~= "table" then SFramesDB.WorldMap = {} end + if SFramesDB.WorldMap.enabled == nil then SFramesDB.WorldMap.enabled = true end + if type(SFramesDB.WorldMap.scale) ~= "number" then SFramesDB.WorldMap.scale = 0.85 end + if type(SFramesDB.WorldMap.nav) ~= "table" then + SFramesDB.WorldMap.nav = { enabled = false, width = 350, alpha = 0.70, locked = false } + end + + if type(SFramesDB.MinimapBuffs) ~= "table" then SFramesDB.MinimapBuffs = {} end + if SFramesDB.MinimapBuffs.enabled == nil then SFramesDB.MinimapBuffs.enabled = true end + if type(SFramesDB.MinimapBuffs.iconSize) ~= "number" then SFramesDB.MinimapBuffs.iconSize = 30 end + if type(SFramesDB.MinimapBuffs.iconsPerRow) ~= "number" then SFramesDB.MinimapBuffs.iconsPerRow = 8 end + if type(SFramesDB.MinimapBuffs.spacing) ~= "number" then SFramesDB.MinimapBuffs.spacing = 2 end + if SFramesDB.MinimapBuffs.growDirection ~= "LEFT" and SFramesDB.MinimapBuffs.growDirection ~= "RIGHT" then + SFramesDB.MinimapBuffs.growDirection = "LEFT" + end + if SFramesDB.MinimapBuffs.position ~= "TOPRIGHT" and SFramesDB.MinimapBuffs.position ~= "TOPLEFT" + and SFramesDB.MinimapBuffs.position ~= "BOTTOMRIGHT" and SFramesDB.MinimapBuffs.position ~= "BOTTOMLEFT" then + SFramesDB.MinimapBuffs.position = "TOPRIGHT" + end + if type(SFramesDB.MinimapBuffs.offsetX) ~= "number" then SFramesDB.MinimapBuffs.offsetX = 0 end + if type(SFramesDB.MinimapBuffs.offsetY) ~= "number" then SFramesDB.MinimapBuffs.offsetY = 0 end + if SFramesDB.MinimapBuffs.showTimer == nil then SFramesDB.MinimapBuffs.showTimer = true end + if SFramesDB.MinimapBuffs.showDebuffs == nil then SFramesDB.MinimapBuffs.showDebuffs = true end + if type(SFramesDB.MinimapBuffs.debuffIconSize) ~= "number" then SFramesDB.MinimapBuffs.debuffIconSize = 30 end + + if type(SFramesDB.ActionBars) ~= "table" then SFramesDB.ActionBars = {} end + local abDef = { + enable = true, buttonSize = 36, buttonGap = 2, smallBarSize = 27, + scale = 1.0, barCount = 3, showHotkey = true, showMacroName = false, + rangeColoring = true, showPetBar = true, showStanceBar = true, + showRightBars = true, buttonRounded = false, buttonInnerShadow = false, + } + for k, v in pairs(abDef) do + if SFramesDB.ActionBars[k] == nil then SFramesDB.ActionBars[k] = v end + end + + if SFramesDB.enableUnitFrames == nil then SFramesDB.enableUnitFrames = true end + if SFramesDB.enableChat == nil then SFramesDB.enableChat = true end + + if type(SFramesDB.Theme) ~= "table" then SFramesDB.Theme = {} end + if SFramesDB.Theme.preset == nil then SFramesDB.Theme.preset = "pink" end + if SFramesDB.Theme.useClassTheme == nil then SFramesDB.Theme.useClassTheme = false end + if SFramesDB.Theme.iconSet == nil or SFramesDB.Theme.iconSet == "default" then SFramesDB.Theme.iconSet = "icon" end +end + +local function CreateSection(parent, title, x, y, width, height, font) + local section = CreateFrame("Frame", NextWidgetName("Section"), parent) + section:SetWidth(width) + section:SetHeight(height) + section:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + + EnsureSoftBackdrop(section) + if section.SetBackdropColor then + section:SetBackdropColor(SOFT_THEME.sectionBg[1], SOFT_THEME.sectionBg[2], SOFT_THEME.sectionBg[3], SOFT_THEME.sectionBg[4]) + end + if section.SetBackdropBorderColor then + section:SetBackdropBorderColor(SOFT_THEME.sectionBorder[1], SOFT_THEME.sectionBorder[2], SOFT_THEME.sectionBorder[3], SOFT_THEME.sectionBorder[4]) + end + + local titleFS = section:CreateFontString(nil, "OVERLAY") + titleFS:SetFont(font, 11, "OUTLINE") + titleFS:SetPoint("TOPLEFT", section, "TOPLEFT", 10, -9) + titleFS:SetText(title) + titleFS:SetTextColor(SOFT_THEME.title[1], SOFT_THEME.title[2], SOFT_THEME.title[3]) + + -- Divider line under title + local div = section:CreateTexture(nil, "ARTWORK") + div:SetTexture("Interface\\Buttons\\WHITE8X8") + div:SetHeight(1) + div:SetPoint("TOPLEFT", section, "TOPLEFT", 8, -24) + div:SetPoint("TOPRIGHT", section, "TOPRIGHT", -8, -24) + div:SetVertexColor(SOFT_THEME.sectionBorder[1], SOFT_THEME.sectionBorder[2], SOFT_THEME.sectionBorder[3], 0.6) + + return section +end + +local function CreateLabel(parent, text, x, y, font, size, r, g, b) + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont(font, size or 11, "OUTLINE") + fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + fs:SetText(text) + fs:SetTextColor(r or 1, g or 1, b or 1) + return fs +end + +local function CreateDesc(parent, text, x, y, font, maxWidth) + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont(font, 9, "OUTLINE") + fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + if maxWidth then + fs:SetWidth(maxWidth) + fs:SetJustifyH("LEFT") + end + fs:SetText(text) + fs:SetTextColor(0.65, 0.58, 0.62) + return fs +end + +local function CreateCheckBox(parent, label, x, y, getter, setter, onValueChanged) + local cb = CreateFrame("CheckButton", NextWidgetName("Check"), parent, "UICheckButtonTemplate") + cb:SetWidth(18) + cb:SetHeight(18) + cb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + StyleCheckBox(cb) + + local text = _G[cb:GetName() .. "Text"] + if text then + text:SetText(label) + local parentWidth = parent:GetWidth() or 520 + local textWidth = parentWidth - x - 24 + if textWidth > 240 then textWidth = 240 end + text:SetWidth(textWidth) + end + + local internalRefresh = false + + cb:SetScript("OnClick", function() + if internalRefresh then return end + local v = this:GetChecked() + local checkedBool = (v == 1 or v == true) + this.sfChecked = checkedBool + if this.sfApplyState then this.sfApplyState(checkedBool) end + setter(checkedBool) + if onValueChanged then onValueChanged(checkedBool) end + PlaySound(checkedBool and "igMainMenuOptionCheckBoxOn" or "igMainMenuOptionCheckBoxOff") + end) + + cb.Refresh = function() + internalRefresh = true + local isChecked = getter() and true or false + cb:SetChecked(isChecked and 1 or 0) + cb.sfChecked = isChecked + if cb.sfApplyState then cb.sfApplyState(isChecked) end + internalRefresh = false + end + + cb:Refresh() + return cb +end + +local function CreateSlider(parent, label, x, y, width, minValue, maxValue, step, getter, setter, formatter, onValueChanged) + local sliderName = NextWidgetName("Slider") + local slider = CreateFrame("Slider", sliderName, parent, "OptionsSliderTemplate") + slider:SetWidth(width) + slider:SetHeight(26) + slider:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + slider:SetMinMaxValues(minValue, maxValue) + slider:SetValueStep(step) + if slider.SetObeyStepOnDrag then slider:SetObeyStepOnDrag(true) end + + local low = _G[sliderName .. "Low"] + local high = _G[sliderName .. "High"] + local text = _G[sliderName .. "Text"] + if low then low:SetText(tostring(minValue)) end + if high then high:SetText(tostring(maxValue)) end + + local internalRefresh = false + local function UpdateLabel(value) + local display = formatter and formatter(value) or value + if text then text:SetText(label .. ": " .. tostring(display)) end + end + + slider:SetScript("OnValueChanged", function() + if internalRefresh then return end + local raw = this:GetValue() or minValue or 0 + local safeStep = step or 1 + local value + if safeStep >= 1 then + value = math.floor(raw + 0.5) + else + if safeStep == 0 then safeStep = 1 end + local scaled = raw / safeStep + value = math.floor(scaled + 0.5) * safeStep + end + value = Clamp(value, minValue, maxValue) + UpdateLabel(value) + setter(value) + if onValueChanged then onValueChanged(value) end + end) + + slider.Refresh = function() + local value = tonumber(getter()) or minValue + value = Clamp(value, minValue, maxValue) + internalRefresh = true + slider:SetValue(value) + internalRefresh = false + UpdateLabel(value) + end + + StyleSlider(slider, low, high, text) + slider:Refresh() + return slider +end + +local function CreateButton(parent, text, x, y, width, height, onClick) + local btn = CreateFrame("Button", NextWidgetName("Button"), parent, "UIPanelButtonTemplate") + btn:SetWidth(width) + btn:SetHeight(height) + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + btn:SetText(text) + btn:SetScript("OnClick", onClick) + StyleButton(btn) + return btn +end +local function CreateScrollArea(parent, x, y, width, height, childHeight) + local holder = CreateFrame("Frame", NextWidgetName("ScrollHolder"), parent) + holder:SetWidth(width) + holder:SetHeight(height) + holder:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + + local scroll = CreateFrame("ScrollFrame", NextWidgetName("ScrollFrame"), holder) + scroll:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0) + scroll:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -18, 0) + + local child = CreateFrame("Frame", NextWidgetName("ScrollChild"), scroll) + child:SetWidth(width - 22) + child:SetHeight(childHeight) + scroll:SetScrollChild(child) + + local sliderName = NextWidgetName("ScrollBar") + local slider = CreateFrame("Slider", sliderName, holder, "UIPanelScrollBarTemplate") + slider:SetPoint("TOPRIGHT", holder, "TOPRIGHT", 0, -16) + slider:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", 0, 16) + slider:SetScript("OnValueChanged", function() + if scroll and scroll.SetVerticalScroll then + scroll:SetVerticalScroll(this:GetValue()) + end + end) + slider:SetMinMaxValues(0, 0) + slider:SetValueStep(20) + slider:SetValue(0) + + local upBtn = _G[sliderName .. "ScrollUpButton"] + if upBtn then + upBtn:SetScript("OnClick", function() + local value = slider:GetValue() - 24 + if value < 0 then value = 0 end + slider:SetValue(value) + end) + end + + local downBtn = _G[sliderName .. "ScrollDownButton"] + if downBtn then + downBtn:SetScript("OnClick", function() + local _, maxValue = slider:GetMinMaxValues() + local value = slider:GetValue() + 24 + if value > maxValue then value = maxValue end + slider:SetValue(value) + end) + end + StyleScrollBar(slider, sliderName) + + local function UpdateRange() + local maxScroll = child:GetHeight() - scroll:GetHeight() + if maxScroll < 0 then maxScroll = 0 end + + slider:SetMinMaxValues(0, maxScroll) + slider:SetValueStep(20) + + local current = slider:GetValue() + if current > maxScroll then + current = maxScroll + slider:SetValue(current) + end + if scroll and scroll.SetVerticalScroll then + scroll:SetVerticalScroll(current) + end + + if maxScroll <= 0 then slider:Hide() else slider:Show() end + end + + local function ScrollBy(delta) + local _, maxValue = slider:GetMinMaxValues() + local value = slider:GetValue() - delta * 28 + if value < 0 then value = 0 end + if value > maxValue then value = maxValue end + slider:SetValue(value) + end + + if scroll.EnableMouseWheel then + scroll:EnableMouseWheel(true) + scroll:SetScript("OnMouseWheel", function() ScrollBy(arg1) end) + end + + if child.EnableMouseWheel then + child:EnableMouseWheel(true) + child:SetScript("OnMouseWheel", function() ScrollBy(arg1) end) + end + + return { + holder = holder, + scroll = scroll, + child = child, + slider = slider, + UpdateRange = UpdateRange, + Reset = function() + slider:SetValue(0) + if scroll and scroll.SetVerticalScroll then + scroll:SetVerticalScroll(0) + end + UpdateRange() + end, + } +end + +function SFrames.ConfigUI:RefreshControls(controlList) + if not controlList then return end + for _, control in ipairs(controlList) do + if control and control.Refresh then control:Refresh() end + end +end + +function SFrames.ConfigUI:BuildUIPage() + local font = SFrames:GetFont() + local page = self.uiPage + local controls = {} + + local function RefreshPlayer() + if SFrames.Player and SFrames.Player.ApplyConfig then SFrames.Player:ApplyConfig() return end + if SFrames.Player and SFrames.Player.UpdateAll then SFrames.Player:UpdateAll() end + end + local function RefreshTarget() + if SFrames.Target and SFrames.Target.ApplyConfig then SFrames.Target:ApplyConfig() return end + if SFrames.Target and SFrames.Target.UpdateAll then SFrames.Target:UpdateAll() end + end + local function RefreshParty() + if SFrames.Party and SFrames.Party.ApplyConfig then SFrames.Party:ApplyConfig() return end + if SFrames.Party and SFrames.Party.UpdateAll then SFrames.Party:UpdateAll() end + end + + -- Section 内容从 y=-32 开始(标题24 + 分隔线 + 8px 间距) + -- 每个选项行占 26px,描述文字占 16px + local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 830) + local root = uiScroll.child + + -- ── 初始化向导 ────────────────────────────────────────────── + local wizSection = CreateSection(root, "初始化向导", 8, -8, 520, 62, font) + CreateDesc(wizSection, "重新打开首次配置向导,可重新选择各项功能开关", 14, -30, font, 380) + CreateButton(wizSection, "重新运行初始化向导", 370, -30, 140, 24, function() + if SFrames.ConfigUI.frame then SFrames.ConfigUI.frame:Hide() end + if SFrames.SetupWizard and SFrames.SetupWizard.Show then + SFrames.SetupWizard:Show(nil, "rerun") + end + end) + + -- ── 全局 ────────────────────────────────────────────────────── + local globalSection = CreateSection(root, "全局", 8, -78, 520, 288, font) + + table.insert(controls, CreateCheckBox(globalSection, + "显示玩家/目标等级文本", 14, -34, + function() return SFramesDB.showLevel ~= false end, + function(checked) SFramesDB.showLevel = checked end, + function() RefreshPlayer() RefreshTarget() RefreshParty() end + )) + CreateDesc(globalSection, "在玩家和目标框体中显示角色等级数字", 36, -50, font) + + table.insert(controls, CreateCheckBox(globalSection, + "玩家/目标生命条使用职业颜色", 14, -70, + function() return SFramesDB.classColorHealth ~= false end, + function(checked) SFramesDB.classColorHealth = checked end, + function() RefreshPlayer() RefreshTarget() RefreshParty() end + )) + CreateDesc(globalSection, "血条颜色跟随职业(关闭则统一绿色)", 36, -86, font) + + table.insert(controls, CreateCheckBox(globalSection, + "启用全新商人购买界面", 14, -106, + function() return SFramesDB.enableMerchant ~= false end, + function(checked) SFramesDB.enableMerchant = checked end + )) + CreateDesc(globalSection, "替换默认商人窗口为自定义界面", 36, -122, font) + + table.insert(controls, CreateCheckBox(globalSection, + "启用全新任务/NPC对话界面", 14, -142, + function() return SFramesDB.enableQuestUI ~= false end, + function(checked) SFramesDB.enableQuestUI = checked end + )) + CreateDesc(globalSection, "替换默认任务和NPC对话窗口(需重载UI)", 36, -158, font) + + table.insert(controls, CreateCheckBox(globalSection, + "启用任务日志美化皮肤", 14, -178, + function() return SFramesDB.enableQuestLogSkin ~= false end, + function(checked) SFramesDB.enableQuestLogSkin = checked end + )) + CreateDesc(globalSection, "美化任务日志界面,兼容 pfQuest(需重载UI)", 36, -194, font) + + table.insert(controls, CreateCheckBox(globalSection, + "启用全新训练师界面", 270, -34, + function() return SFramesDB.enableTrainer ~= false end, + function(checked) SFramesDB.enableTrainer = checked end + )) + CreateDesc(globalSection, "替换默认职业/专业训练师窗口(需重载UI)", 292, -50, font) + + table.insert(controls, CreateCheckBox(globalSection, + "启用全新法术书界面", 270, -70, + function() return SFramesDB.enableSpellBook ~= false end, + function(checked) SFramesDB.enableSpellBook = checked end + )) + CreateDesc(globalSection, "替换默认法术书窗口(需重载UI)", 292, -86, font) + + table.insert(controls, CreateCheckBox(globalSection, + "启用全新社交界面", 270, -106, + function() return SFramesDB.enableSocial ~= false end, + function(checked) SFramesDB.enableSocial = checked end + )) + CreateDesc(globalSection, "替换好友/查询/工会/团队窗口(需重载UI)", 292, -122, font) + + table.insert(controls, CreateCheckBox(globalSection, + "NPC单选项自动跳过", 270, -142, + function() return SFramesDB.autoGossip ~= false end, + function(checked) SFramesDB.autoGossip = checked end + )) + CreateDesc(globalSection, "只有一个对话选项时自动确认(按 Shift 临时禁用)", 292, -158, font) + + table.insert(controls, CreateCheckBox(globalSection, + "启用兽栏皮肤", 270, -178, + function() return SFramesDB.enablePetStable ~= false end, + function(checked) SFramesDB.enablePetStable = checked end + )) + CreateDesc(globalSection, "美化宠物兽栏界面(需重载UI)", 292, -194, font) + + table.insert(controls, CreateCheckBox(globalSection, + "启用全新专业技能界面", 14, -214, + function() return SFramesDB.enableTradeSkill ~= false end, + function(checked) SFramesDB.enableTradeSkill = checked end + )) + CreateDesc(globalSection, "替换默认专业技能窗口(需重载UI)", 36, -230, font) + + table.insert(controls, CreateCheckBox(globalSection, + "启用全新飞行地图界面", 270, -214, + function() return SFramesDB.enableFlightMap ~= false end, + function(checked) SFramesDB.enableFlightMap = checked end + )) + CreateDesc(globalSection, "美化飞行管理员地图,增加目的地列表(需重载UI)", 292, -230, font) + + table.insert(controls, CreateCheckBox(globalSection, + "启用全新邮箱界面", 14, -250, + function() return SFramesDB.enableMail ~= false end, + function(checked) SFramesDB.enableMail = checked end + )) + CreateDesc(globalSection, "替换默认邮箱窗口,支持批量收取和多物品发送(需重载UI)", 36, -266, font) + + -- ── 增强功能(Libs 库集成)────────────────────────────────── + local enhSection = CreateSection(root, "增强功能(需安装 !Libs 插件)", 8, -376, 520, 204, font) + + table.insert(controls, CreateCheckBox(enhSection, + "血条平滑动画(需 /reload)", 14, -34, + function() return SFramesDB.smoothBars ~= false end, + function(checked) SFramesDB.smoothBars = checked end + )) + CreateDesc(enhSection, "血量/法力变化时丝滑过渡,需要 LibSmoothStatusBar", 36, -50, font, 218) + + table.insert(controls, CreateCheckBox(enhSection, + "目标真实血量", 270, -34, + function() return SFramesDB.mobRealHealth ~= false end, + function(checked) SFramesDB.mobRealHealth = checked end + )) + CreateDesc(enhSection, "怪物血条显示实际HP数值,需要 LibMobHealthCache", 292, -50, font, 218) + + table.insert(controls, CreateCheckBox(enhSection, + "物品等级显示", 14, -80, + function() return SFramesDB.showItemLevel ~= false end, + function(checked) SFramesDB.showItemLevel = checked end, + function() + if SFrames.CharacterPanel and SFrames.CharacterPanel.UpdateCurrentTab then + SFrames.CharacterPanel:UpdateCurrentTab() + end + end + )) + CreateDesc(enhSection, "装备栏/背包角标显示 iLvl + 面板平均装等,需要 LibItem-Level", 36, -96, font, 218) + + table.insert(controls, CreateCheckBox(enhSection, + "装备属性对比", 270, -80, + function() return SFramesDB.itemCompare ~= false end, + function(checked) SFramesDB.itemCompare = checked end + )) + CreateDesc(enhSection, "背包物品 tooltip 显示与已装备的属性差异,需要 ItemBonusLib", 292, -96, font, 218) + + table.insert(controls, CreateCheckBox(enhSection, + "装备评分 (EP)", 14, -126, + function() return SFramesDB.gearScore ~= false end, + function(checked) SFramesDB.gearScore = checked end + )) + CreateDesc(enhSection, "Tooltip 显示当前职业各天赋EP评分及硬核评分,需要 ItemBonusLib", 36, -142, font, 480) + + CreateLabel(enhSection, "提示:以上功能需要安装对应的 !Libs 库才能生效。", 14, -172, font, 10, 0.6, 0.6, 0.65) + + -- ── ShaguTweaks 功能移植 ────────────────────────────────────── + local tweaksSection = CreateSection(root, "ShaguTweaks 功能移植(需 /reload 生效)", 8, -590, 520, 214, font) + + table.insert(controls, CreateCheckBox(tweaksSection, + "自动切换姿态/形态", 14, -34, + function() return SFramesDB.Tweaks.autoStance ~= false end, + function(checked) SFramesDB.Tweaks.autoStance = checked end + )) + CreateDesc(tweaksSection, "施法需要特定姿态时自动切换(如人形按熊掌→自动变熊)", 36, -50, font, 218) + + table.insert(controls, CreateCheckBox(tweaksSection, + "自动取消变形/下马", 270, -34, + function() return SFramesDB.Tweaks.autoDismount ~= false end, + function(checked) SFramesDB.Tweaks.autoDismount = checked end + )) + CreateDesc(tweaksSection, "变形/骑马时施法自动取消(如熊形按治疗→自动回人形)", 292, -50, font, 218) + + table.insert(controls, CreateCheckBox(tweaksSection, + "乌龟服兼容修改", 14, -80, + function() return SFramesDB.Tweaks.turtleCompat ~= false end, + function(checked) SFramesDB.Tweaks.turtleCompat = checked end + )) + CreateDesc(tweaksSection, "隐藏乌龟服自带目标血量文字,修复地图窗口标题偏移", 36, -96, font, 218) + + table.insert(controls, CreateCheckBox(tweaksSection, + "SuperWoW 客户端兼容", 270, -80, + function() return SFramesDB.Tweaks.superWoW ~= false end, + function(checked) SFramesDB.Tweaks.superWoW = checked end + )) + CreateDesc(tweaksSection, "为 SuperWoW 提供 GUID 施法数据支持(需安装 SuperWoW)", 292, -96, font, 218) + + table.insert(controls, CreateCheckBox(tweaksSection, + "冷却倒计时", 14, -126, + function() return SFramesDB.Tweaks.cooldownNumbers ~= false end, + function(checked) SFramesDB.Tweaks.cooldownNumbers = checked end + )) + CreateDesc(tweaksSection, "在技能/物品冷却图标上显示剩余时间文字(>2秒)", 36, -142, font, 218) + + table.insert(controls, CreateCheckBox(tweaksSection, + "暗色界面风格", 270, -126, + function() return SFramesDB.Tweaks.darkUI == true end, + function(checked) SFramesDB.Tweaks.darkUI = checked end + )) + CreateDesc(tweaksSection, "将整个游戏界面调暗为深色主题(默认关闭)", 292, -142, font, 218) + + CreateLabel(tweaksSection, "提示:以上所有选项修改后需要 /reload 才能生效。", 14, -172, font, 10, 0.6, 0.6, 0.65) + + uiScroll:UpdateRange() + self.uiControls = controls + self.uiScroll = uiScroll +end + + +function SFrames.ConfigUI:BuildPlayerPage() + local font = SFrames:GetFont() + local page = self.playerPage + local controls = {} + + local function RefreshPlayer() + if SFrames.Player and SFrames.Player.ApplyConfig then + SFrames.Player:ApplyConfig() + return + end + if SFrames.Player and SFrames.Player.UpdateAll then SFrames.Player:UpdateAll() end + end + + local playerSection = CreateSection(page, "玩家框体", 8, -8, 520, 254, font) + + table.insert(controls, CreateSlider(playerSection, "缩放", 14, -46, 150, 0.7, 1.8, 0.05, + function() return SFramesDB.playerFrameScale end, + function(value) SFramesDB.playerFrameScale = value end, + function(v) return string.format("%.2f", v) end, + function() RefreshPlayer() end + )) + + table.insert(controls, CreateSlider(playerSection, "框体宽度", 170, -46, 150, 170, 420, 1, + function() return SFramesDB.playerFrameWidth end, + function(value) SFramesDB.playerFrameWidth = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshPlayer() end + )) + + table.insert(controls, CreateSlider(playerSection, "头像宽度", 326, -46, 130, 32, 95, 1, + function() return SFramesDB.playerPortraitWidth end, + function(value) SFramesDB.playerPortraitWidth = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshPlayer() end + )) + + table.insert(controls, CreateSlider(playerSection, "生命条高度", 14, -108, 150, 14, 80, 1, + function() return SFramesDB.playerHealthHeight end, + function(value) SFramesDB.playerHealthHeight = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshPlayer() end + )) + + table.insert(controls, CreateSlider(playerSection, "能量条高度", 170, -108, 150, 6, 40, 1, + function() return SFramesDB.playerPowerHeight end, + function(value) SFramesDB.playerPowerHeight = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshPlayer() end + )) + + table.insert(controls, CreateCheckBox(playerSection, + "玩家姓名显示职业", 326, -106, + function() return SFramesDB.playerShowClass ~= false end, + function(checked) SFramesDB.playerShowClass = checked end, + function() RefreshPlayer() end + )) + + table.insert(controls, CreateCheckBox(playerSection, + "玩家显示职业图标", 326, -132, + function() return SFramesDB.playerShowClassIcon ~= false end, + function(checked) SFramesDB.playerShowClassIcon = checked end, + function() RefreshPlayer() end + )) + + table.insert(controls, CreateSlider(playerSection, "姓名字号", 14, -170, 150, 8, 18, 1, + function() return SFramesDB.playerNameFontSize end, + function(value) SFramesDB.playerNameFontSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshPlayer() end + )) + + table.insert(controls, CreateSlider(playerSection, "数值字号", 170, -170, 150, 8, 18, 1, + function() return SFramesDB.playerValueFontSize end, + function(value) SFramesDB.playerValueFontSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshPlayer() end + )) + + CreateLabel(playerSection, + "提示:宽度/头像/血条/能量条可独立实时调节。", + 14, -221, font, 10, 0.9, 0.9, 0.9) + + -- ── 宠物框体 ────────────────────────────────────────────────── + local petSection = CreateSection(page, "宠物框体", 8, -270, 520, 98, font) + + table.insert(controls, CreateCheckBox(petSection, + "显示宠物框体", 14, -34, + function() return SFramesDB.showPetFrame ~= false end, + function(checked) SFramesDB.showPetFrame = checked end, + function() if SFrames.Pet and SFrames.Pet.UpdateAll then SFrames.Pet:UpdateAll() end end + )) + CreateDesc(petSection, "在玩家框体附近显示当前宠物的血量条", 36, -50, font) + + table.insert(controls, CreateSlider(petSection, "缩放", 270, -52, 200, 0.7, 1.8, 0.05, + function() return SFramesDB.petFrameScale or 1 end, + function(value) SFramesDB.petFrameScale = value end, + function(v) return string.format("%.2f", v) end, + function(value) if SFrames.Pet and SFrames.Pet.frame then SFrames.Pet.frame:SetScale(value) end end + )) + + self.playerControls = controls +end + +function SFrames.ConfigUI:BuildTargetPage() + local font = SFrames:GetFont() + local page = self.targetPage + local controls = {} + + local function RefreshTarget() + if SFrames.Target and SFrames.Target.ApplyConfig then + SFrames.Target:ApplyConfig() + return + end + if SFrames.Target and SFrames.Target.UpdateAll then SFrames.Target:UpdateAll() end + end + + local targetSection = CreateSection(page, "目标框体", 8, -8, 520, 294, font) + + table.insert(controls, CreateSlider(targetSection, "缩放", 14, -46, 150, 0.7, 1.8, 0.05, + function() return SFramesDB.targetFrameScale end, + function(value) SFramesDB.targetFrameScale = value end, + function(v) return string.format("%.2f", v) end, + function() RefreshTarget() end + )) + + table.insert(controls, CreateSlider(targetSection, "框体宽度", 170, -46, 150, 170, 420, 1, + function() return SFramesDB.targetFrameWidth end, + function(value) SFramesDB.targetFrameWidth = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshTarget() end + )) + + table.insert(controls, CreateSlider(targetSection, "头像宽度", 326, -46, 130, 32, 95, 1, + function() return SFramesDB.targetPortraitWidth end, + function(value) SFramesDB.targetPortraitWidth = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshTarget() end + )) + + table.insert(controls, CreateSlider(targetSection, "生命条高度", 14, -108, 150, 14, 80, 1, + function() return SFramesDB.targetHealthHeight end, + function(value) SFramesDB.targetHealthHeight = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshTarget() end + )) + + table.insert(controls, CreateSlider(targetSection, "能量条高度", 170, -108, 150, 6, 40, 1, + function() return SFramesDB.targetPowerHeight end, + function(value) SFramesDB.targetPowerHeight = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshTarget() end + )) + + table.insert(controls, CreateCheckBox(targetSection, + "目标姓名显示职业", 326, -106, + function() return SFramesDB.targetShowClass ~= false end, + function(checked) SFramesDB.targetShowClass = checked end, + function() RefreshTarget() end + )) + + table.insert(controls, CreateCheckBox(targetSection, + "目标显示职业图标", 326, -132, + function() return SFramesDB.targetShowClassIcon ~= false end, + function(checked) SFramesDB.targetShowClassIcon = checked end, + function() RefreshTarget() end + )) + + table.insert(controls, CreateSlider(targetSection, "姓名字号", 14, -170, 150, 8, 18, 1, + function() return SFramesDB.targetNameFontSize end, + function(value) SFramesDB.targetNameFontSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshTarget() end + )) + + table.insert(controls, CreateSlider(targetSection, "数值字号", 170, -170, 150, 8, 18, 1, + function() return SFramesDB.targetValueFontSize end, + function(value) SFramesDB.targetValueFontSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshTarget() end + )) + + table.insert(controls, CreateCheckBox(targetSection, + "启用目标距离文本", 326, -168, + function() return SFramesDB.targetDistanceEnabled ~= false end, + function(checked) SFramesDB.targetDistanceEnabled = checked end, + function(checked) + if SFrames.Target and SFrames.Target.distanceFrame then + if checked and UnitExists("target") then + SFrames.Target.distanceFrame:Show() + elseif not checked then + SFrames.Target.distanceFrame:Hide() + end + end + end + )) + + table.insert(controls, CreateSlider(targetSection, "距离文本缩放", 14, -232, 150, 0.7, 1.8, 0.05, + function() return SFramesDB.targetDistanceScale end, + function(value) SFramesDB.targetDistanceScale = value end, + function(v) return string.format("%.2f", v) end, + function(value) + if SFrames.Target and SFrames.Target.distanceFrame then + SFrames.Target.distanceFrame:SetScale(value) + end + end + )) + + self.targetControls = controls +end + +function SFrames.ConfigUI:BuildPartyPage() + local font = SFrames:GetFont() + local page = self.partyPage + local controls = {} + + local function RefreshParty() + if SFrames.Party and SFrames.Party.ApplyConfig then + SFrames.Party:ApplyConfig() + end + if SFrames.Party and SFrames.Party.UpdateAll then SFrames.Party:UpdateAll() end + end + + local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 320) + local root = uiScroll.child + + local partySection = CreateSection(root, "小队", 8, -8, 520, 356, font) + + CreateButton(partySection, "解锁小队框架", 14, -26, 130, 22, function() + if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end + end) + + CreateButton(partySection, "锁定小队框架", 154, -26, 130, 22, function() + if SFrames and SFrames.LockFrames then SFrames:LockFrames() end + end) + + table.insert(controls, CreateCheckBox(partySection, + "横向布局(关闭为竖向)", 12, -60, + function() return SFramesDB.partyLayout == "horizontal" end, + function(checked) + if checked then SFramesDB.partyLayout = "horizontal" else SFramesDB.partyLayout = "vertical" end + end, + function(checked) + if SFrames.Party and SFrames.Party.SetLayout then + if checked then SFrames.Party:SetLayout("horizontal") else SFrames.Party:SetLayout("vertical") end + end + RefreshParty() + end + )) + + table.insert(controls, CreateSlider(partySection, "缩放", 14, -108, 150, 0.7, 1.8, 0.05, + function() return SFramesDB.partyFrameScale end, + function(value) SFramesDB.partyFrameScale = value end, + function(v) return string.format("%.2f", v) end, + function() RefreshParty() end + )) + + table.insert(controls, CreateSlider(partySection, "框体宽度", 170, -108, 150, 120, 320, 1, + function() return SFramesDB.partyFrameWidth end, + function(value) SFramesDB.partyFrameWidth = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshParty() end + )) + + table.insert(controls, CreateSlider(partySection, "框体高度", 326, -108, 130, 28, 80, 1, + function() return SFramesDB.partyFrameHeight end, + function(value) SFramesDB.partyFrameHeight = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshParty() end + )) + + table.insert(controls, CreateSlider(partySection, "头像宽度", 14, -170, 150, 24, 64, 1, + function() return SFramesDB.partyPortraitWidth end, + function(value) SFramesDB.partyPortraitWidth = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshParty() end + )) + + table.insert(controls, CreateSlider(partySection, "生命条高度", 170, -170, 150, 10, 60, 1, + function() return SFramesDB.partyHealthHeight end, + function(value) SFramesDB.partyHealthHeight = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshParty() end + )) + + table.insert(controls, CreateSlider(partySection, "能量条高度", 326, -170, 130, 6, 30, 1, + function() return SFramesDB.partyPowerHeight end, + function(value) SFramesDB.partyPowerHeight = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshParty() end + )) + + table.insert(controls, CreateSlider(partySection, "横向间距", 14, -232, 150, 0, 40, 1, + function() return SFramesDB.partyHorizontalGap end, + function(value) SFramesDB.partyHorizontalGap = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshParty() end + )) + + table.insert(controls, CreateSlider(partySection, "纵向间距", 170, -232, 150, 0, 80, 1, + function() return SFramesDB.partyVerticalGap end, + function(value) SFramesDB.partyVerticalGap = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshParty() end + )) + + table.insert(controls, CreateSlider(partySection, "姓名字号", 326, -232, 130, 8, 18, 1, + function() return SFramesDB.partyNameFontSize end, + function(value) SFramesDB.partyNameFontSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshParty() end + )) + + table.insert(controls, CreateSlider(partySection, "数值字号", 14, -294, 150, 8, 18, 1, + function() return SFramesDB.partyValueFontSize end, + function(value) SFramesDB.partyValueFontSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshParty() end + )) + + table.insert(controls, CreateCheckBox(partySection, + "显示小队增益", 170, -292, + function() return SFramesDB.partyShowBuffs ~= false end, + function(checked) SFramesDB.partyShowBuffs = checked end, + function() RefreshParty() end + )) + + table.insert(controls, CreateCheckBox(partySection, + "显示小队减益", 326, -292, + function() return SFramesDB.partyShowDebuffs ~= false end, + function(checked) SFramesDB.partyShowDebuffs = checked end, + function() RefreshParty() end + )) + + CreateButton(partySection, "小队测试模式", 326, -324, 130, 22, function() + if SFrames.Party and SFrames.Party.TestMode then SFrames.Party:TestMode() end + end) + uiScroll:UpdateRange() + self.partyControls = controls + self.partyScroll = uiScroll +end + +function SFrames.ConfigUI:BuildBagPage() + local font = SFrames:GetFont() + local page = self.bagPage + local controls = {} + + local bagScroll = CreateScrollArea(page, 4, -4, 548, 458, 510) + local root = bagScroll.child + + local bagSection = CreateSection(root, "背包", 8, -8, 502, 196, font) + + table.insert(controls, CreateCheckBox(bagSection, + "启用 Nanami 背包(需 /reload)", 12, -28, + function() return SFramesDB.Bags.enable ~= false end, + function(checked) SFramesDB.Bags.enable = checked end + )) + + table.insert(controls, CreateCheckBox(bagSection, + "自动出售灰色物品", 12, -52, + function() return SFramesDB.Bags.sellGrey ~= false end, + function(checked) SFramesDB.Bags.sellGrey = checked end + )) + + table.insert(controls, CreateSlider(bagSection, "背包列数", 16, -90, 220, 4, 20, 1, + function() return SFramesDB.Bags.columns end, + function(value) SFramesDB.Bags.columns = value end, + function(value) return tostring(math.floor(value + 0.5)) end, + function() + if SFrames and SFrames.Bags and SFrames.Bags.Container and SFrames.Bags.Container.UpdateLayout then + SFrames.Bags.Container:UpdateLayout() + end + end + )) + + table.insert(controls, CreateSlider(bagSection, "背包间距", 258, -90, 220, 0, 12, 1, + function() return SFramesDB.Bags.bagSpacing end, + function(value) SFramesDB.Bags.bagSpacing = value end, + function(value) return tostring(math.floor(value + 0.5)) end, + function() + if SFrames and SFrames.Bags and SFrames.Bags.Container and SFrames.Bags.Container.UpdateLayout then + SFrames.Bags.Container:UpdateLayout() + end + end + )) + + table.insert(controls, CreateSlider(bagSection, "背包缩放", 16, -150, 220, 0.50, 2.00, 0.05, + function() return SFramesDB.Bags.scale end, + function(value) SFramesDB.Bags.scale = value end, + function(value) return string.format("%.2f", value) end, + function(value) + if SFramesBagFrame then SFramesBagFrame:SetScale(value) end + end + )) + + CreateButton(bagSection, "打开背包", 258, -150, 220, 24, function() + if SFrames and SFrames.Bags and SFrames.Bags.Container and SFrames.Bags.Container.Open then + SFrames.Bags.Container:Open() + end + end) + + local bankSection = CreateSection(root, "银行", 8, -210, 502, 155, font) + + table.insert(controls, CreateSlider(bankSection, "银行列数", 16, -50, 220, 4, 24, 1, + function() return SFramesDB.Bags.bankColumns end, + function(value) SFramesDB.Bags.bankColumns = value end, + function(value) return tostring(math.floor(value + 0.5)) end, + function() + if SFrames and SFrames.Bags and SFrames.Bags.Bank and SFrames.Bags.Bank.UpdateLayout then + SFrames.Bags.Bank:UpdateLayout() + end + end + )) + + table.insert(controls, CreateSlider(bankSection, "银行间距", 258, -50, 220, 0, 12, 1, + function() return SFramesDB.Bags.bankSpacing end, + function(value) SFramesDB.Bags.bankSpacing = value end, + function(value) return tostring(math.floor(value + 0.5)) end, + function() + if SFrames and SFrames.Bags and SFrames.Bags.Bank and SFrames.Bags.Bank.UpdateLayout then + SFrames.Bags.Bank:UpdateLayout() + end + end + )) + + table.insert(controls, CreateSlider(bankSection, "银行缩放", 16, -110, 220, 0.50, 2.00, 0.05, + function() return SFramesDB.Bags.bankScale end, + function(value) SFramesDB.Bags.bankScale = value end, + function(value) return string.format("%.2f", value) end, + function(value) + if SFramesBankFrame then SFramesBankFrame:SetScale(value) end + end + )) + + CreateButton(bankSection, "打开离线银行", 258, -110, 220, 24, function() + if SFrames and SFrames.Bags and SFrames.Bags.Bank and SFrames.Bags.Bank.OpenOffline then + SFrames.Bags.Bank:OpenOffline() + end + end) + + --------------------------------------------------------------------------- + -- Sell Price Database Section + --------------------------------------------------------------------------- + local priceSection = CreateSection(root, "售价数据库", 8, -371, 502, 126, font) + + local function CountCacheEntries() + local n = 0 + if SFramesGlobalDB and SFramesGlobalDB.sellPriceCache then + for _ in pairs(SFramesGlobalDB.sellPriceCache) do n = n + 1 end + end + return n + end + + local function CountDBEntries() + local n = 0 + if NanamiSellPriceDB then + for _ in pairs(NanamiSellPriceDB) do n = n + 1 end + end + return n + end + + local dbCount = CountDBEntries() + local statusLabel = CreateLabel(priceSection, + string.format("内置: %d 条 | 已学习: %d 条", dbCount, CountCacheEntries()), + 14, -30, font, 10, 0.8, 0.8, 0.7) + + local scanResultLabel = CreateLabel(priceSection, "", 14, -104, font, 10, 0.45, 0.9, 0.45) + + CreateButton(priceSection, "扫描全部物品售价", 16, -48, 228, 24, function() + if not SFramesGlobalDB then SFramesGlobalDB = {} end + if not SFramesGlobalDB.sellPriceCache then SFramesGlobalDB.sellPriceCache = {} end + local cache = SFramesGlobalDB.sellPriceCache + local scanned, learned = 0, 0 + + local function TryScan(link) + if not link then return end + local _, _, sid = string.find(link, "item:(%d+)") + if not sid then return end + local id = tonumber(sid) + if not id then return end + scanned = scanned + 1 + if cache[id] and cache[id] > 0 then return end + + local price + if NanamiSellPriceDB and NanamiSellPriceDB[id] and NanamiSellPriceDB[id] > 0 then + price = NanamiSellPriceDB[id] + elseif ShaguTweaks and ShaguTweaks.SellValueDB and ShaguTweaks.SellValueDB[id] and ShaguTweaks.SellValueDB[id] > 0 then + price = ShaguTweaks.SellValueDB[id] + else + local _,_,_,_,_,_,_,_,_,_,sp = GetItemInfo(link) + if sp and type(sp) == "number" and sp > 0 then price = sp end + end + if price then + cache[id] = price + learned = learned + 1 + end + end + + for bag = 0, 4 do + for slot = 1, (GetContainerNumSlots(bag) or 0) do + TryScan(GetContainerItemLink(bag, slot)) + end + end + for bag = 5, 10 do + pcall(function() + for slot = 1, (GetContainerNumSlots(bag) or 0) do + TryScan(GetContainerItemLink(bag, slot)) + end + end) + end + pcall(function() + for slot = 1, (GetContainerNumSlots(-1) or 0) do + TryScan(GetContainerItemLink(-1, slot)) + end + end) + for slot = 1, 19 do + TryScan(GetInventoryItemLink("player", slot)) + end + + statusLabel:SetText(string.format("内置: %d 条 | 已学习: %d 条", dbCount, CountCacheEntries())) + scanResultLabel:SetText(string.format("完成! 检查 %d 件, 新增 %d 条售价", scanned, learned)) + end) + + CreateButton(priceSection, "清空学习缓存", 258, -48, 228, 24, function() + if SFramesGlobalDB then SFramesGlobalDB.sellPriceCache = {} end + statusLabel:SetText(string.format("内置: %d 条 | 已学习: 0 条", dbCount)) + scanResultLabel:SetText("学习缓存已清空") + end) + + CreateDesc(priceSection, "扫描背包/银行/装备中所有物品, 缓存售价到离线数据库 (随存档保存)", 14, -76, font, 480) + CreateDesc(priceSection, "多次游玩后数据自动丰富, 其他角色也能共享离线数据", 14, -90, font, 480) + + bagScroll:UpdateRange() + self.bagScroll = bagScroll + self.bagControls = controls +end + +function SFrames.ConfigUI:BuildRaidPage() + local font = SFrames:GetFont() + local page = self.raidPage + local controls = {} + + local function RefreshRaid() + if SFrames.Raid and SFrames.Raid.ApplyLayout then + if not SFrames.Raid._framesBuilt then return end + SFrames.Raid:ApplyLayout() + SFrames.Raid:UpdateAll() + for i = 1, 40 do + local f = SFrames.Raid.frames[i] and SFrames.Raid.frames[i].frame + if f then SFrames.Raid:ApplyFrameStyle(f, SFrames.Raid:GetMetrics()) end + end + end + end + + local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 600) + local root = uiScroll.child + + local raidSection = CreateSection(root, "团队框架设置", 8, -8, 520, 570, font) + + table.insert(controls, CreateCheckBox(raidSection, + "启用 Nanami 团队框架(默认启用,需 /reload)", 12, -28, + function() return SFramesDB.enableRaidFrames ~= false end, + function(checked) SFramesDB.enableRaidFrames = checked end + )) + + CreateButton(raidSection, "解锁团队框架", 14, -56, 130, 22, function() + if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end + end) + + CreateButton(raidSection, "锁定团队框架", 154, -56, 130, 22, function() + if SFrames and SFrames.LockFrames then SFrames:LockFrames() end + end) + + table.insert(controls, CreateCheckBox(raidSection, + "横向小队排列(关闭为竖向叠放)", 12, -88, + function() return SFramesDB.raidLayout == "horizontal" end, + function(checked) + SFramesDB.raidLayout = checked and "horizontal" or "vertical" + end, + function() RefreshRaid() end + )) + + table.insert(controls, CreateSlider(raidSection, "缩放", 14, -150, 150, 0.5, 2.0, 0.05, + function() return SFramesDB.raidFrameScale end, + function(value) SFramesDB.raidFrameScale = value end, + function(v) return string.format("%.2f", v) end, + function(val) + if SFrames.Raid and SFrames.Raid.parent then + SFrames.Raid.parent:SetScale(val) + end + end + )) + + table.insert(controls, CreateSlider(raidSection, "框体宽度", 170, -150, 150, 30, 150, 1, + function() return SFramesDB.raidFrameWidth end, + function(value) SFramesDB.raidFrameWidth = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshRaid() end + )) + + table.insert(controls, CreateSlider(raidSection, "框体高度", 326, -150, 130, 20, 80, 1, + function() return SFramesDB.raidFrameHeight end, + function(value) SFramesDB.raidFrameHeight = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshRaid() end + )) + + table.insert(controls, CreateSlider(raidSection, "生命条高度", 14, -212, 150, 10, 78, 1, + function() return SFramesDB.raidHealthHeight end, + function(value) SFramesDB.raidHealthHeight = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshRaid() end + )) + + table.insert(controls, CreateCheckBox(raidSection, + "显示能量条", 170, -210, + function() return SFramesDB.raidShowPower ~= false end, + function(checked) SFramesDB.raidShowPower = checked end, + function() RefreshRaid() end + )) + + table.insert(controls, CreateCheckBox(raidSection, + "显示小队标题(横向=顶部,竖向=左侧)", 14, -244, + function() return SFramesDB.raidShowGroupLabel ~= false end, + function(checked) SFramesDB.raidShowGroupLabel = checked end, + function() RefreshRaid() end + )) + + table.insert(controls, CreateSlider(raidSection, "横向间距", 14, -274, 150, 0, 40, 1, + function() return SFramesDB.raidHorizontalGap end, + function(value) SFramesDB.raidHorizontalGap = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshRaid() end + )) + + table.insert(controls, CreateSlider(raidSection, "纵向间距", 170, -274, 150, 0, 40, 1, + function() return SFramesDB.raidVerticalGap end, + function(value) SFramesDB.raidVerticalGap = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshRaid() end + )) + + table.insert(controls, CreateSlider(raidSection, "小队间隔", 326, -274, 130, 0, 60, 1, + function() return SFramesDB.raidGroupGap end, + function(value) SFramesDB.raidGroupGap = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshRaid() end + )) + + table.insert(controls, CreateSlider(raidSection, "姓名字号", 14, -336, 150, 8, 18, 1, + function() return SFramesDB.raidNameFontSize end, + function(value) SFramesDB.raidNameFontSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshRaid() end + )) + + table.insert(controls, CreateSlider(raidSection, "数值字号", 170, -336, 150, 8, 18, 1, + function() return SFramesDB.raidValueFontSize end, + function(value) SFramesDB.raidValueFontSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshRaid() end + )) + + table.insert(controls, CreateCheckBox(raidSection, + "血量百分比模式 (关=紧凑数值/-缺口)", 14, -390, + function() return SFramesDB.raidHealthFormat == "percent" end, + function(checked) SFramesDB.raidHealthFormat = checked and "percent" or "compact" end, + function() RefreshRaid() end + )) + + CreateButton(raidSection, "测试模式(假团队)", 14, -450, 150, 24, function() + if SFrames.Raid then + if SFrames.Raid.testing then + SFrames.Raid.testing = false + SFrames.Raid:UpdateAll() + else + SFrames.Raid.testing = true + SFrames.Raid:EnsureFrames() + SFrames.Raid.parent:Show() + for i = 1, 40 do + local f = SFrames.Raid.frames[i].frame + f:Show() + f.nameText:SetText("玩家"..i) + f.healthText:SetText("100%") + f.health:SetMinMaxValues(0, 100) + f.health:SetValue(100) + f.health:SetStatusBarColor(0, 1, 0) + end + end + end + end) + + uiScroll:UpdateRange() + self.raidControls = controls + self.raidScroll = uiScroll +end + +function SFrames.ConfigUI:BuildCharPage() + local font = SFrames:GetFont() + local page = self.charPage + local controls = {} + + local uiScroll = CreateScrollArea(page, 4, -4, 552, PANEL_HEIGHT - 110, 720) + local root = uiScroll.child + + local charSection = CreateSection(root, "人物面板", 8, -8, 524, 430, font) + + table.insert(controls, CreateCheckBox(charSection, + "启用 Nanami 人物面板(需 /reload)", 12, -34, + function() return SFramesDB.charPanelEnable ~= false end, + function(checked) SFramesDB.charPanelEnable = checked end + )) + CreateDesc(charSection, "关闭后按 C 键将打开原版人物面板", 36, -50, font) + + table.insert(controls, CreateSlider(charSection, + "面板缩放", 14, -72, 240, 0.6, 1.5, 0.05, + function() return SFramesDB.charPanelScale or 1.0 end, + function(v) SFramesDB.charPanelScale = v end, + function(v) return string.format("%.0f%%", v * 100) end, + function() + if SFramesCharacterPanel then + SFramesCharacterPanel:SetScale(SFramesDB.charPanelScale or 1.0) + end + end + )) + + table.insert(controls, CreateCheckBox(charSection, + "显示PVP称号", 12, -118, + function() return SFramesDB.charShowTitle ~= false end, + function(checked) SFramesDB.charShowTitle = checked end, + function() + if SFrames.CharacterPanel and SFrames.CharacterPanel.UpdateCurrentTab then + SFrames.CharacterPanel:UpdateCurrentTab() + end + end + )) + + table.insert(controls, CreateCheckBox(charSection, + "显示公会名", 12, -146, + function() return SFramesDB.charShowGuild ~= false end, + function(checked) SFramesDB.charShowGuild = checked end, + function() + if SFrames.CharacterPanel and SFrames.CharacterPanel.UpdateCurrentTab then + SFrames.CharacterPanel:UpdateCurrentTab() + end + end + )) + + table.insert(controls, CreateCheckBox(charSection, + "显示装备品质光晕", 12, -174, + function() return SFramesDB.charShowQualityGlow ~= false end, + function(checked) SFramesDB.charShowQualityGlow = checked end, + function() + if SFrames.CharacterPanel and SFrames.CharacterPanel.UpdateCurrentTab then + SFrames.CharacterPanel:UpdateCurrentTab() + end + end + )) + + table.insert(controls, CreateCheckBox(charSection, + "显示3D角色模型", 12, -202, + function() return SFramesDB.charShowModel ~= false end, + function(checked) SFramesDB.charShowModel = checked end, + function() + if SFrames.CharacterPanel and SFrames.CharacterPanel.UpdateCurrentTab then + SFrames.CharacterPanel:UpdateCurrentTab() + end + end + )) + + table.insert(controls, CreateCheckBox(charSection, + "启用 Nanami 观察面板(需 /reload)", 12, -230, + function() return SFramesDB.enableInspect ~= false end, + function(checked) SFramesDB.enableInspect = checked end + )) + CreateDesc(charSection, "关闭后观察他人将使用原版界面", 36, -246, font) + + CreateLabel(charSection, "提示: 面板使用 C 键打开/关闭,按 ESC 关闭。", 14, -274, font, 10, 0.6, 0.6, 0.65) + CreateLabel(charSection, "装备页下方可滚动查看全部角色属性。", 14, -292, font, 10, 0.6, 0.6, 0.65) + + -- Spell Book section + local sbSection = CreateSection(root, "法术书", 8, -446, 524, 200, font) + + table.insert(controls, CreateSlider(sbSection, + "法术书缩放", 14, -34, 240, 0.6, 1.5, 0.05, + function() return SFramesDB.spellBookScale or 1.0 end, + function(v) SFramesDB.spellBookScale = v end, + function(v) return string.format("%.0f%%", v * 100) end, + function() + if SFramesSpellBookUI then + SFramesSpellBookUI:SetScale(SFramesDB.spellBookScale or 1.0) + end + end + )) + + table.insert(controls, CreateCheckBox(sbSection, + "只显示最高等级法术", 14, -80, + function() return SFramesDB.spellBookHighestOnly == true end, + function(checked) SFramesDB.spellBookHighestOnly = checked end + )) + CreateDesc(sbSection, "隐藏同名法术的低等级版本,只保留最高等级", 36, -96, font) + + table.insert(controls, CreateCheckBox(sbSection, + "学习新等级后自动替换动作条", 14, -116, + function() return SFramesDB.spellBookAutoReplace == true end, + function(checked) SFramesDB.spellBookAutoReplace = checked end + )) + CreateDesc(sbSection, "学习高等级法术时,自动替换动作条上的低等级版本", 36, -132, font) + + CreateLabel(sbSection, "提示: 法术书使用 P 键打开/关闭,支持鼠标滚轮翻页。", 14, -160, font, 10, 0.6, 0.6, 0.65) + + -- Social section + local socialSection = CreateSection(root, "社交面板", 8, -660, 524, 120, font) + + table.insert(controls, CreateSlider(socialSection, + "社交面板缩放", 14, -34, 240, 0.6, 1.5, 0.05, + function() return SFramesDB.socialScale or 1.0 end, + function(v) SFramesDB.socialScale = v end, + function(v) return string.format("%.0f%%", v * 100) end, + function() + if SFramesSocialUI then + SFramesSocialUI:SetScale(SFramesDB.socialScale or 1.0) + end + end + )) + + CreateLabel(socialSection, "提示: 社交面板使用 O 键打开/关闭。", 14, -80, font, 10, 0.6, 0.6, 0.65) + + uiScroll:UpdateRange() + self.charControls = controls + self.charScroll = uiScroll +end + +function SFrames.ConfigUI:BuildActionBarPage() + local font = SFrames:GetFont() + local page = self.actionBarPage + local controls = {} + + local function RefreshAB() + if SFrames.ActionBars and SFrames.ActionBars.ApplyConfig then + SFrames.ActionBars:ApplyConfig() + end + end + + local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 978) + local root = uiScroll.child + + local abSection = CreateSection(root, "动作条", 8, -8, 520, 948, font) + + table.insert(controls, CreateCheckBox(abSection, + "启用动作条接管(需 /reload 生效)", 12, -28, + function() return SFramesDB.ActionBars.enable ~= false end, + function(checked) SFramesDB.ActionBars.enable = checked end + )) + + CreateLabel(abSection, "按键绑定:", 14, -56, font, 11, 0.85, 0.75, 0.80) + + CreateButton(abSection, "进入按键绑定模式", 14, -76, 160, 24, function() + if SFrames.ActionBars and SFrames.ActionBars.EnterKeyBindMode then + if self.frame then self.frame:Hide() end + SFrames.ActionBars:EnterKeyBindMode() + end + end) + + CreateDesc(abSection, + "悬停动作条/姿态栏/宠物栏按钮,按下键盘键或滚轮绑定。右键清除。ESC 退出。也可用 /nui bind", + 14, -100, font, 480) + + table.insert(controls, CreateSlider(abSection, "按钮大小", 14, -130, 150, 24, 48, 1, + function() return SFramesDB.ActionBars.buttonSize end, + function(value) SFramesDB.ActionBars.buttonSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + table.insert(controls, CreateSlider(abSection, "按钮间距", 180, -130, 150, 0, 8, 1, + function() return SFramesDB.ActionBars.buttonGap end, + function(value) SFramesDB.ActionBars.buttonGap = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + table.insert(controls, CreateSlider(abSection, "整体缩放", 346, -130, 150, 0.5, 2.0, 0.05, + function() return SFramesDB.ActionBars.scale end, + function(value) SFramesDB.ActionBars.scale = value end, + function(v) return string.format("%.2f", v) end, + function() RefreshAB() end + )) + + table.insert(controls, CreateSlider(abSection, "显示行数", 14, -192, 150, 1, 3, 1, + function() return SFramesDB.ActionBars.barCount end, + function(value) SFramesDB.ActionBars.barCount = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + table.insert(controls, CreateSlider(abSection, "姿态/宠物栏按钮大小", 180, -192, 150, 16, 40, 1, + function() return SFramesDB.ActionBars.smallBarSize end, + function(value) SFramesDB.ActionBars.smallBarSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + table.insert(controls, CreateCheckBox(abSection, + "显示快捷键文字", 12, -242, + function() return SFramesDB.ActionBars.showHotkey ~= false end, + function(checked) SFramesDB.ActionBars.showHotkey = checked end, + function() RefreshAB() end + )) + + table.insert(controls, CreateCheckBox(abSection, + "显示宏名称", 200, -242, + function() return SFramesDB.ActionBars.showMacroName == true end, + function(checked) SFramesDB.ActionBars.showMacroName = checked end, + function() RefreshAB() end + )) + + table.insert(controls, CreateCheckBox(abSection, + "超距红色着色(技能超出射程时按钮变红)", 12, -270, + function() return SFramesDB.ActionBars.rangeColoring ~= false end, + function(checked) SFramesDB.ActionBars.rangeColoring = checked end + )) + + table.insert(controls, CreateCheckBox(abSection, + "显示宠物动作条", 12, -298, + function() return SFramesDB.ActionBars.showPetBar ~= false end, + function(checked) SFramesDB.ActionBars.showPetBar = checked end, + function() RefreshAB() end + )) + + table.insert(controls, CreateCheckBox(abSection, + "显示姿态栏", 200, -298, + function() return SFramesDB.ActionBars.showStanceBar ~= false end, + function(checked) SFramesDB.ActionBars.showStanceBar = checked end, + function() RefreshAB() end + )) + + table.insert(controls, CreateCheckBox(abSection, + "显示右侧动作条(两列竖向栏)", 12, -326, + function() return SFramesDB.ActionBars.showRightBars ~= false end, + function(checked) SFramesDB.ActionBars.showRightBars = checked end, + function() RefreshAB() end + )) + + table.insert(controls, CreateCheckBox(abSection, + "始终显示动作条(空格子也显示背景框)", 12, -354, + function() return SFramesDB.ActionBars.alwaysShowGrid == true end, + function(checked) SFramesDB.ActionBars.alwaysShowGrid = checked end, + function() RefreshAB() end + )) + + table.insert(controls, CreateCheckBox(abSection, + "按钮圆角", 12, -382, + function() return SFramesDB.ActionBars.buttonRounded == true end, + function(checked) SFramesDB.ActionBars.buttonRounded = checked end, + function() RefreshAB() end + )) + + table.insert(controls, CreateCheckBox(abSection, + "按钮内阴影", 200, -382, + function() return SFramesDB.ActionBars.buttonInnerShadow == true end, + function(checked) SFramesDB.ActionBars.buttonInnerShadow = checked end, + function() RefreshAB() end + )) + + table.insert(controls, CreateCheckBox(abSection, + "显示动作条狮鹫(在底部动作条两侧显示装饰狮鹫)", 12, -410, + function() return SFramesDB.ActionBars.hideGryphon == false end, + function(checked) SFramesDB.ActionBars.hideGryphon = not checked end, + function() RefreshAB() end + )) + + table.insert(controls, CreateCheckBox(abSection, + "狮鹫置于动作条之上(否则在动作条之下)", 12, -438, + function() return SFramesDB.ActionBars.gryphonOnTop == true end, + function(checked) SFramesDB.ActionBars.gryphonOnTop = checked end, + function() RefreshAB() end + )) + + table.insert(controls, CreateSlider(abSection, "狮鹫宽度", 14, -492, 150, 24, 200, 2, + function() return SFramesDB.ActionBars.gryphonWidth or 64 end, + function(value) SFramesDB.ActionBars.gryphonWidth = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + table.insert(controls, CreateSlider(abSection, "狮鹫高度", 180, -492, 150, 24, 200, 2, + function() return SFramesDB.ActionBars.gryphonHeight or 64 end, + function(value) SFramesDB.ActionBars.gryphonHeight = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + table.insert(controls, CreateSlider(abSection, "水平偏移(向内重叠)", 14, -554, 150, -50, 100, 1, + function() return SFramesDB.ActionBars.gryphonOffsetX or 30 end, + function(value) SFramesDB.ActionBars.gryphonOffsetX = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + table.insert(controls, CreateSlider(abSection, "垂直偏移", 180, -554, 150, -100, 100, 1, + function() return SFramesDB.ActionBars.gryphonOffsetY or 0 end, + function(value) SFramesDB.ActionBars.gryphonOffsetY = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + -- 狮鹫样式选择器(带图例预览) + CreateLabel(abSection, "狮鹫样式:", 14, -606, font, 11, 0.85, 0.75, 0.80) + + local GRYPHON_STYLES_UI = { + { key = "dragonflight", label = "巨龙时代", + tex = "Interface\\AddOns\\Nanami-UI\\img\\df-gryphon" }, + { key = "dragonflight_beta", label = "巨龙时代(Beta)", + tex = "Interface\\AddOns\\Nanami-UI\\img\\df-gryphon-beta" }, + { key = "classic", label = "经典", + tex = "Interface\\MainMenuBar\\UI-MainMenuBar-EndCap-Human" }, + { key = "cat", label = "猫", + tex = "Interface\\AddOns\\Nanami-UI\\img\\cat" }, + } + + local styleBorders = {} + local styleStartX = 14 + local styleY = -626 + + for idx, style in ipairs(GRYPHON_STYLES_UI) do + local xOff = styleStartX + (idx - 1) * 125 + + -- 预览框 + local preview = CreateFrame("Frame", nil, abSection) + preview:SetWidth(64) + preview:SetHeight(64) + preview:SetPoint("TOPLEFT", abSection, "TOPLEFT", xOff, styleY) + + -- 预览背景 + local bg = preview:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints() + bg:SetTexture(0, 0, 0, 0.4) + + -- 纹理预览 + local tex = preview:CreateTexture(nil, "ARTWORK") + tex:SetAllPoints() + tex:SetTexture(style.tex) + + -- 选中边框 + local border = CreateFrame("Frame", nil, preview) + border:SetPoint("TOPLEFT", preview, "TOPLEFT", -2, 2) + border:SetPoint("BOTTOMRIGHT", preview, "BOTTOMRIGHT", 2, -2) + border:SetBackdrop({ + edgeFile = "Interface\\Buttons\\WHITE8X8", + edgeSize = 2, + }) + border:Hide() + styleBorders[idx] = border + + -- 样式名标签 + local label = preview:CreateFontString(nil, "OVERLAY") + label:SetFont(font, 10, "OUTLINE") + label:SetPoint("TOP", preview, "BOTTOM", 0, -4) + label:SetText(style.label) + label:SetTextColor(0.7, 0.65, 0.7) + + -- 点击选择 + preview:EnableMouse(true) + preview._sfStyleKey = style.key + preview._sfIdx = idx + preview:SetScript("OnMouseUp", function() + SFramesDB.ActionBars.gryphonStyle = this._sfStyleKey + for i, bd in ipairs(styleBorders) do + if i == this._sfIdx then + bd:Show() + bd:SetBackdropBorderColor(1, 0.78, 0.2, 1) + else + bd:Hide() + end + end + RefreshAB() + end) + + -- 悬停高亮 + preview:SetScript("OnEnter", function() + local bd = styleBorders[this._sfIdx] + if bd and not bd:IsShown() then + bd:Show() + bd:SetBackdropBorderColor(SOFT_THEME.panelBorder[1], SOFT_THEME.panelBorder[2], SOFT_THEME.panelBorder[3], 0.8) + end + end) + preview:SetScript("OnLeave", function() + local bd = styleBorders[this._sfIdx] + local current = SFramesDB.ActionBars.gryphonStyle or "dragonflight" + if bd and this._sfStyleKey ~= current then + bd:Hide() + end + end) + end + + -- 初始化选中边框 + local currentStyle = SFramesDB.ActionBars.gryphonStyle or "dragonflight" + for idx, style in ipairs(GRYPHON_STYLES_UI) do + if style.key == currentStyle then + styleBorders[idx]:Show() + styleBorders[idx]:SetBackdropBorderColor(1, 0.78, 0.2, 1) + end + end + + CreateLabel(abSection, "底部动作条位置:", 14, -720, font, 11, 0.85, 0.75, 0.80) + + table.insert(controls, CreateSlider(abSection, "底部 X 偏移", 14, -756, 220, -500, 500, 1, + function() return SFramesDB.ActionBars.bottomOffsetX or 0 end, + function(value) SFramesDB.ActionBars.bottomOffsetX = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + table.insert(controls, CreateSlider(abSection, "底部 Y 偏移", 270, -756, 220, 0, 500, 1, + function() return SFramesDB.ActionBars.bottomOffsetY or 2 end, + function(value) SFramesDB.ActionBars.bottomOffsetY = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + CreateLabel(abSection, "右侧动作条位置:", 14, -812, font, 11, 0.85, 0.75, 0.80) + + table.insert(controls, CreateSlider(abSection, "右侧 X 偏移", 14, -848, 220, -500, 0, 1, + function() return SFramesDB.ActionBars.rightOffsetX or -4 end, + function(value) SFramesDB.ActionBars.rightOffsetX = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + table.insert(controls, CreateSlider(abSection, "右侧 Y 偏移", 270, -848, 220, -500, 500, 1, + function() return SFramesDB.ActionBars.rightOffsetY or -80 end, + function(value) SFramesDB.ActionBars.rightOffsetY = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshAB() end + )) + + CreateLabel(abSection, + "提示:启用/禁用动作条接管需要 /reload 才能生效。", + 14, -904, font, 10, 1, 0.92, 0.38) + + uiScroll:UpdateRange() + self.actionBarControls = controls + self.actionBarScroll = uiScroll +end + +function SFrames.ConfigUI:BuildMinimapPage() + local font = SFrames:GetFont() + local page = self.minimapPage + local controls = {} + + local function RefreshMinimap() + if SFrames.Minimap and SFrames.Minimap.Refresh then + SFrames.Minimap:Refresh() + end + end + + local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 782) + local root = uiScroll.child + + local mmSection = CreateSection(root, "小地图", 8, -8, 520, 540, font) + + table.insert(controls, CreateCheckBox(mmSection, + "启用 Nanami 小地图皮肤", 14, -36, + function() return SFramesDB.Minimap.enabled ~= false end, + function(checked) SFramesDB.Minimap.enabled = checked end + )) + CreateDesc(mmSection, "关闭后恢复默认小地图,需要 /reload 生效", 36, -52, font) + + table.insert(controls, CreateSlider(mmSection, "缩放", 14, -80, 220, 0.6, 2.0, 0.05, + function() return SFramesDB.Minimap.scale or 1.0 end, + function(value) SFramesDB.Minimap.scale = value end, + function(v) return string.format("%.0f%%", v * 100) end, + function() RefreshMinimap() end + )) + + table.insert(controls, CreateCheckBox(mmSection, + "显示游戏时钟", 14, -134, + function() return SFramesDB.Minimap.showClock ~= false end, + function(checked) SFramesDB.Minimap.showClock = checked end + )) + CreateDesc(mmSection, "在小地图下方显示服务器时间", 36, -150, font) + + table.insert(controls, CreateCheckBox(mmSection, + "显示玩家坐标", 270, -134, + function() return SFramesDB.Minimap.showCoords ~= false end, + function(checked) SFramesDB.Minimap.showCoords = checked end + )) + CreateDesc(mmSection, "在小地图圆内底部显示当前坐标", 292, -150, font) + + local function ApplyPos() + if SFrames.Minimap and SFrames.Minimap.ApplyPosition then + SFrames.Minimap.ApplyPosition() + end + end + + table.insert(controls, CreateSlider(mmSection, "水平位置 (X)", 14, -182, 220, -800, 0, 1, + function() return SFramesDB.Minimap.posX or -5 end, + function(value) SFramesDB.Minimap.posX = value end, + function(v) return string.format("%.0f", v) end, + function() ApplyPos() end + )) + + table.insert(controls, CreateSlider(mmSection, "垂直位置 (Y)", 270, -182, 220, -800, 0, 1, + function() return SFramesDB.Minimap.posY or -5 end, + function(value) SFramesDB.Minimap.posY = value end, + function(v) return string.format("%.0f", v) end, + function() ApplyPos() end + )) + + CreateButton(mmSection, "重置位置", 14, -226, 130, 22, function() + SFramesDB.Minimap.posX = -5 + SFramesDB.Minimap.posY = -5 + ApplyPos() + end) + + -- Map frame style selector with texture previews (5 columns x 2 rows) + CreateLabel(mmSection, "边框样式:", 14, -256, font, 11, 0.85, 0.75, 0.80) + + local styles = SFrames.Minimap and SFrames.Minimap.MAP_STYLES or {} + + -- "Auto (match class)" button + local autoBtn = CreateFrame("Button", nil, mmSection) + autoBtn:SetWidth(100) + autoBtn:SetHeight(18) + autoBtn:SetPoint("TOPLEFT", mmSection, "TOPLEFT", 130, -256) + autoBtn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 8, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + + local autoLabel = autoBtn:CreateFontString(nil, "OVERLAY") + autoLabel:SetFont(font, 10, "OUTLINE") + autoLabel:SetPoint("CENTER", autoBtn, "CENTER", 0, 0) + autoLabel:SetText("自动(匹配职业)") + + local function UpdateAutoBtn(isAuto) + if isAuto then + autoBtn:SetBackdropColor(0.2, 0.4, 0.2, 0.9) + autoBtn:SetBackdropBorderColor(0.4, 0.8, 0.3, 1) + autoLabel:SetTextColor(0.5, 1, 0.5) + else + autoBtn:SetBackdropColor(0.15, 0.15, 0.15, 0.7) + autoBtn:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.6) + autoLabel:SetTextColor(0.6, 0.6, 0.6) + end + end + + local styleBorders = {} + local cols = 5 + local cellW = 56 + local cellH = 56 + local gapX = 10 + local gapY = 18 + local styleStartX = 14 + local styleY = -280 + + local function HighlightSelection() + local current = SFramesDB.Minimap.mapStyle or "auto" + local isAuto = (current == "auto") + UpdateAutoBtn(isAuto) + for i, bd in ipairs(styleBorders) do + if (not isAuto) and styles[i] and styles[i].key == current then + bd:Show() + bd:SetBackdropBorderColor(1, 0.78, 0.2, 1) + else + bd:Hide() + end + end + end + + for idx, style in ipairs(styles) do + local col = math.mod(idx - 1, cols) + local row = math.floor((idx - 1) / cols) + local xOff = styleStartX + col * (cellW + gapX) + local yOff = styleY - row * (cellH + gapY) + + local preview = CreateFrame("Frame", nil, mmSection) + preview:SetWidth(cellW) + preview:SetHeight(cellH) + preview:SetPoint("TOPLEFT", mmSection, "TOPLEFT", xOff, yOff) + + local bg = preview:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints() + bg:SetTexture(0, 0, 0, 0.4) + + local ptex = preview:CreateTexture(nil, "ARTWORK") + ptex:SetAllPoints() + ptex:SetTexture(style.tex) + + local border = CreateFrame("Frame", nil, preview) + border:SetPoint("TOPLEFT", preview, "TOPLEFT", -2, 2) + border:SetPoint("BOTTOMRIGHT", preview, "BOTTOMRIGHT", 2, -2) + border:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 2 }) + border:Hide() + styleBorders[idx] = border + + local label = preview:CreateFontString(nil, "OVERLAY") + label:SetFont(font, 9, "OUTLINE") + label:SetPoint("TOP", preview, "BOTTOM", 0, -2) + label:SetText(style.label) + label:SetTextColor(0.7, 0.65, 0.7) + + preview:EnableMouse(true) + preview._sfStyleKey = style.key + preview._sfIdx = idx + preview:SetScript("OnMouseUp", function() + SFramesDB.Minimap.mapStyle = this._sfStyleKey + HighlightSelection() + RefreshMinimap() + end) + preview:SetScript("OnEnter", function() + local bd = styleBorders[this._sfIdx] + if bd and not bd:IsShown() then + bd:Show() + bd:SetBackdropBorderColor(SOFT_THEME.panelBorder[1], SOFT_THEME.panelBorder[2], SOFT_THEME.panelBorder[3], 0.8) + end + end) + preview:SetScript("OnLeave", function() + local bd = styleBorders[this._sfIdx] + local current = SFramesDB.Minimap.mapStyle or "auto" + if bd and this._sfStyleKey ~= current then + bd:Hide() + end + end) + end + + autoBtn:SetScript("OnClick", function() + SFramesDB.Minimap.mapStyle = "auto" + HighlightSelection() + RefreshMinimap() + end) + + HighlightSelection() + + local totalRows = math.ceil(table.getn(styles) / cols) + local tipY = styleY - totalRows * (cellH + gapY) - 10 + CreateLabel(mmSection, "提示: 缩放、位置和样式修改后实时生效。", 14, tipY, font, 10, 0.6, 0.6, 0.65) + + -- ── 世界地图 ────────────────────────────────────────────────── + local wmSection = CreateSection(root, "世界地图", 8, -558, 520, 130, font) + + table.insert(controls, CreateCheckBox(wmSection, + "启用全新世界地图界面", 14, -34, + function() return SFramesDB.WorldMap.enabled == true end, + function(checked) + SFramesDB.WorldMap.enabled = checked + if checked then SFramesDB.Tweaks.worldMapWindow = false end + end + )) + CreateDesc(wmSection, "Nanami主题窗口化地图,隐藏原生装饰(需重载UI)", 36, -50, font) + + table.insert(controls, CreateCheckBox(wmSection, + "启用导航地图", 14, -68, + function() + return SFramesDB.WorldMap.nav and SFramesDB.WorldMap.nav.enabled == true + end, + function(checked) + if type(SFramesDB.WorldMap.nav) ~= "table" then +SFramesDB.WorldMap.nav = { enabled = false, width = 350, alpha = 0.70, locked = false } + end + SFramesDB.WorldMap.nav.enabled = checked + if SFrames.WorldMap and SFrames.WorldMap.ToggleNav then + if (checked and not (NanamiNavMap and NanamiNavMap:IsVisible())) + or (not checked and NanamiNavMap and NanamiNavMap:IsVisible()) then + SFrames.WorldMap:ToggleNav() + end + end + end + )) + CreateDesc(wmSection, "以玩家为中心的实时导航地图,半透明覆盖,可拖动", 36, -84, font) + CreateDesc(wmSection, "命令: /nui nav | 滚轮缩放 | Ctrl+滚轮透明度", 36, -96, font) + + -- ── 地图迷雾揭示 ─────────────────────────────────────────── + local mapRevealSection = CreateSection(root, "世界地图迷雾揭示", 8, -698, 520, 112, font) + + table.insert(controls, CreateCheckBox(mapRevealSection, + "启用地图迷雾揭示", 14, -34, + function() return SFramesDB.MapReveal.enabled ~= false end, + function(checked) + SFramesDB.MapReveal.enabled = checked + if SFrames.MapReveal and SFrames.MapReveal.Refresh then + SFrames.MapReveal:Refresh() + end + end + )) + CreateDesc(mapRevealSection, "在世界地图上显示未探索区域(变暗显示)", 36, -50, font) + CreateDesc(mapRevealSection, "需要 LibMapOverlayData 库支持", 36, -62, font) + + table.insert(controls, CreateSlider(mapRevealSection, "未探索区域亮度", 270, -64, 200, 0.2, 1.0, 0.05, + function() return SFramesDB.MapReveal.unexploredAlpha or 0.7 end, + function(value) SFramesDB.MapReveal.unexploredAlpha = value end, + function(v) return string.format("%.0f%%", v * 100) end, + function(value) + if SFrames.MapReveal and SFrames.MapReveal.SetAlpha then + SFrames.MapReveal:SetAlpha(value) + end + end + )) + + uiScroll:UpdateRange() + self.minimapControls = controls + self.minimapScroll = uiScroll +end + +-------------------------------------------------------------------------------- +-- Buff / Debuff Page (standalone tab) +-------------------------------------------------------------------------------- +function SFrames.ConfigUI:BuildBuffPage() + local font = SFrames:GetFont() + local page = self.buffPage + local controls = {} + + local function RefreshBuffs() + if SFrames.MinimapBuffs and SFrames.MinimapBuffs.Refresh then + SFrames.MinimapBuffs:Refresh() + end + end + + local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 520) + local root = uiScroll.child + + local buffSection = CreateSection(root, "Buff / Debuff 栏", 8, -8, 520, 490, font) + + -- Row 1: enable + show timer + table.insert(controls, CreateCheckBox(buffSection, + "启用 Buff / Debuff 栏", 14, -36, + function() return SFramesDB.MinimapBuffs.enabled ~= false end, + function(checked) SFramesDB.MinimapBuffs.enabled = checked end + )) + CreateDesc(buffSection, "替代暴雪默认 Buff 栏显示(需 /reload 生效)", 36, -52, font) + + table.insert(controls, CreateCheckBox(buffSection, + "显示时间文本", 270, -36, + function() return SFramesDB.MinimapBuffs.showTimer ~= false end, + function(checked) + SFramesDB.MinimapBuffs.showTimer = checked + RefreshBuffs() + end + )) + CreateDesc(buffSection, "图标下方显示剩余时间,永久 Buff 显示 N/A", 292, -52, font) + + -- Row 2: show debuffs + table.insert(controls, CreateCheckBox(buffSection, + "显示 Debuff", 14, -72, + function() return SFramesDB.MinimapBuffs.showDebuffs ~= false end, + function(checked) + SFramesDB.MinimapBuffs.showDebuffs = checked + RefreshBuffs() + end + )) + CreateDesc(buffSection, "在 Buff 栏下方显示 Debuff(边框按类型着色)", 36, -88, font) + + -- Row 3: sliders - icon sizes + table.insert(controls, CreateSlider(buffSection, "Buff 图标大小", 14, -114, 220, 20, 50, 1, + function() return SFramesDB.MinimapBuffs.iconSize or 30 end, + function(value) SFramesDB.MinimapBuffs.iconSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshBuffs() end + )) + + table.insert(controls, CreateSlider(buffSection, "Debuff 图标大小", 270, -114, 220, 20, 50, 1, + function() return SFramesDB.MinimapBuffs.debuffIconSize or 30 end, + function(value) SFramesDB.MinimapBuffs.debuffIconSize = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshBuffs() end + )) + + -- Row 4: per-row + spacing + table.insert(controls, CreateSlider(buffSection, "每行图标数", 14, -176, 220, 4, 16, 1, + function() return SFramesDB.MinimapBuffs.iconsPerRow or 8 end, + function(value) SFramesDB.MinimapBuffs.iconsPerRow = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshBuffs() end + )) + + table.insert(controls, CreateSlider(buffSection, "间距", 270, -176, 220, 0, 8, 1, + function() return SFramesDB.MinimapBuffs.spacing or 2 end, + function(value) SFramesDB.MinimapBuffs.spacing = value end, + function(v) return tostring(math.floor(v + 0.5)) end, + function() RefreshBuffs() end + )) + + -- Row 5: grow direction + anchor + CreateLabel(buffSection, "增长方向:", 14, -240, font, 11, 0.85, 0.75, 0.80) + CreateButton(buffSection, "向左", 100, -240, 70, 22, function() + SFramesDB.MinimapBuffs.growDirection = "LEFT" + RefreshBuffs() + end) + CreateButton(buffSection, "向右", 176, -240, 70, 22, function() + SFramesDB.MinimapBuffs.growDirection = "RIGHT" + RefreshBuffs() + end) + + CreateLabel(buffSection, "锚点位置:", 270, -240, font, 11, 0.85, 0.75, 0.80) + local posLabels = { TOPRIGHT = "右上", TOPLEFT = "左上", BOTTOMRIGHT = "右下", BOTTOMLEFT = "左下" } + local posKeys = { "TOPRIGHT", "TOPLEFT", "BOTTOMRIGHT", "BOTTOMLEFT" } + local posStartX = 356 + for _, key in ipairs(posKeys) do + CreateButton(buffSection, posLabels[key], posStartX, -240, 40, 22, function() + SFramesDB.MinimapBuffs.position = key + RefreshBuffs() + end) + posStartX = posStartX + 42 + end + + -- Row 6: X/Y offset sliders + table.insert(controls, CreateSlider(buffSection, "水平偏移 (X)", 14, -286, 220, -800, 800, 1, + function() return SFramesDB.MinimapBuffs.offsetX or 0 end, + function(value) SFramesDB.MinimapBuffs.offsetX = value end, + function(v) return string.format("%.0f", v) end, + function() RefreshBuffs() end + )) + + table.insert(controls, CreateSlider(buffSection, "垂直偏移 (Y)", 270, -286, 220, -800, 800, 1, + function() return SFramesDB.MinimapBuffs.offsetY or 0 end, + function(value) SFramesDB.MinimapBuffs.offsetY = value end, + function(v) return string.format("%.0f", v) end, + function() RefreshBuffs() end + )) + + -- Row 7: action buttons + CreateButton(buffSection, "重置默认位置", 14, -340, 120, 24, function() + SFramesDB.MinimapBuffs.offsetX = 0 + SFramesDB.MinimapBuffs.offsetY = 0 + SFramesDB.MinimapBuffs.position = "TOPRIGHT" + SFramesDB.MinimapBuffs.growDirection = "LEFT" + RefreshBuffs() + end) + + local simBtn + simBtn = CreateButton(buffSection, "模拟预览", 142, -340, 100, 24, function() + if not SFrames.MinimapBuffs then return end + if SFrames.MinimapBuffs._simulating then + SFrames.MinimapBuffs:StopSimulation() + simBtn:SetText("模拟预览") + else + SFrames.MinimapBuffs:SimulateBuffs() + simBtn:SetText("停止模拟") + end + end) + + -- Row 8: tips + CreateLabel(buffSection, "模拟预览:显示假 Buff / Debuff 以预览布局效果,不影响实际状态。", 14, -374, font, 9, 0.65, 0.58, 0.62) + CreateLabel(buffSection, "Debuff 边框颜色: |cff3399ff魔法|r |cff9900ff诅咒|r |cff996600疾病|r |cff009900毒药|r |cffcc0000物理|r", 14, -390, font, 9, 0.65, 0.58, 0.62) + + CreateLabel(buffSection, "提示:启用/禁用 Buff 栏需要 /reload 才能生效。其他调整实时生效。", 14, -414, font, 10, 0.6, 0.6, 0.65) + + uiScroll:UpdateRange() + self.buffControls = controls + self.buffScroll = uiScroll +end + +function SFrames.ConfigUI:ShowPage(mode) + self.activePage = mode + + self.uiPage:Hide() + self.playerPage:Hide() + self.targetPage:Hide() + self.bagPage:Hide() + self.raidPage:Hide() + self.partyPage:Hide() + self.charPage:Hide() + self.actionBarPage:Hide() + self.minimapPage:Hide() + self.buffPage:Hide() + self.personalizePage:Hide() + self.themePage:Hide() + + local allTabs = { self.uiTab, self.playerTab, self.targetTab, self.partyTab, self.raidTab, self.bagTab, self.charTab, self.actionBarTab, self.minimapTab, self.buffTab, self.personalizeTab, self.themeTab } + for _, tab in ipairs(allTabs) do + tab.sfSoftActive = false + tab:Enable() + tab:RefreshVisual() + end + + self:EnsurePage(mode or "ui") + + if mode == "player" then + self.playerPage:Show() + self.playerTab.sfSoftActive = true + self.playerTab:Disable() + self.playerTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 玩家框体") + self:RefreshControls(self.playerControls) + elseif mode == "target" then + self.targetPage:Show() + self.targetTab.sfSoftActive = true + self.targetTab:Disable() + self.targetTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 目标框体") + self:RefreshControls(self.targetControls) + elseif mode == "bags" then + self.bagPage:Show() + self.bagTab.sfSoftActive = true + self.bagTab:Disable() + self.bagTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 背包") + self:RefreshControls(self.bagControls) + if self.bagScroll and self.bagScroll.Reset then self.bagScroll:Reset() end + elseif mode == "raid" then + self.raidPage:Show() + self.raidTab.sfSoftActive = true + self.raidTab:Disable() + self.raidTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 团队") + self:RefreshControls(self.raidControls) + if self.raidScroll and self.raidScroll.Reset then self.raidScroll:Reset() end + elseif mode == "party" then + self.partyPage:Show() + self.partyTab.sfSoftActive = true + self.partyTab:Disable() + self.partyTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 小队") + self:RefreshControls(self.partyControls) + if self.partyScroll and self.partyScroll.Reset then self.partyScroll:Reset() end + elseif mode == "char" then + self.charPage:Show() + self.charTab.sfSoftActive = true + self.charTab:Disable() + self.charTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 人物面板") + self:RefreshControls(self.charControls) + if self.charScroll and self.charScroll.Reset then self.charScroll:Reset() end + elseif mode == "actionbar" then + self.actionBarPage:Show() + self.actionBarTab.sfSoftActive = true + self.actionBarTab:Disable() + self.actionBarTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 动作条") + self:RefreshControls(self.actionBarControls) + if self.actionBarScroll and self.actionBarScroll.Reset then self.actionBarScroll:Reset() end + elseif mode == "minimap" then + self.minimapPage:Show() + self.minimapTab.sfSoftActive = true + self.minimapTab:Disable() + self.minimapTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 地图设置") + self:RefreshControls(self.minimapControls) + if self.minimapScroll and self.minimapScroll.Reset then self.minimapScroll:Reset() end + elseif mode == "buff" then + self.buffPage:Show() + self.buffTab.sfSoftActive = true + self.buffTab:Disable() + self.buffTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - Buff / Debuff") + self:RefreshControls(self.buffControls) + if self.buffScroll and self.buffScroll.Reset then self.buffScroll:Reset() end + elseif mode == "personalize" then + self.personalizePage:Show() + self.personalizeTab.sfSoftActive = true + self.personalizeTab:Disable() + self.personalizeTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 个性化") + self:RefreshControls(self.personalizeControls) + if self.personalizeScroll and self.personalizeScroll.Reset then self.personalizeScroll:Reset() end + elseif mode == "theme" then + self.themePage:Show() + self.themeTab.sfSoftActive = true + self.themeTab:Disable() + self.themeTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 主题") + self:RefreshControls(self.themeControls) + if self.themeScroll and self.themeScroll.Reset then self.themeScroll:Reset() end + else + self.uiPage:Show() + self.uiTab.sfSoftActive = true + self.uiTab:Disable() + self.uiTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 界面") + self:RefreshControls(self.uiControls) + if self.uiScroll and self.uiScroll.Reset then self.uiScroll:Reset() end + end +end + +function SFrames.ConfigUI:EnsureFrame() + if self.frame then return end + + local font = SFrames:GetFont() + + local panel = CreateFrame("Frame", "SFramesConfigPanel", UIParent) + panel:SetWidth(PANEL_WIDTH) + panel:SetHeight(PANEL_HEIGHT) + panel:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + panel:SetMovable(true) + panel:EnableMouse(true) + panel:SetClampedToScreen(true) + panel:SetToplevel(true) + panel:SetFrameStrata("TOOLTIP") + panel:SetFrameLevel(100) + panel:RegisterForDrag("LeftButton") + panel:SetScript("OnDragStart", function() this:StartMoving() end) + panel:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + tinsert(UISpecialFrames, "SFramesConfigPanel") + + -- Apply the rounded "Roll UI" textured backdrop to the main settings panel + if SFrames and SFrames.CreateRoundBackdrop then + SFrames:CreateRoundBackdrop(panel) + panel.sfSoftBackdrop = true + else + EnsureSoftBackdrop(panel) + end + + if panel.SetBackdropColor then + panel:SetBackdropColor(SOFT_THEME.panelBg[1], SOFT_THEME.panelBg[2], SOFT_THEME.panelBg[3], SOFT_THEME.panelBg[4]) + end + if panel.SetBackdropBorderColor then + panel:SetBackdropBorderColor(SOFT_THEME.panelBorder[1], SOFT_THEME.panelBorder[2], SOFT_THEME.panelBorder[3], SOFT_THEME.panelBorder[4]) + end + + local titleIco = SFrames:CreateIcon(panel, "logo", 18) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("TOP", panel, "TOP", -52, -14) + titleIco:SetVertexColor(SOFT_THEME.title[1], SOFT_THEME.title[2], SOFT_THEME.title[3]) + + local title = panel:CreateFontString(nil, "OVERLAY") + title:SetFont(font, 14, "OUTLINE") + title:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) + title:SetTextColor(SOFT_THEME.title[1], SOFT_THEME.title[2], SOFT_THEME.title[3]) + title:SetText("Nanami-UI 设置") + + local divider = panel:CreateTexture(nil, "ARTWORK") + divider:SetWidth(1) + divider:SetPoint("TOPLEFT", panel, "TOPLEFT", 120, -40) + divider:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 120, 10) + divider:SetTexture(0.44, 0.42, 0.5, 0.9) + + local closeBtn = CreateFrame("Button", "SFramesConfigCloseBtn", panel) + closeBtn:SetWidth(20) + closeBtn:SetHeight(20) + closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -8, -8) + closeBtn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + local _cA = SOFT_THEME + closeBtn:SetBackdropColor(_cA.buttonDownBg[1], _cA.buttonDownBg[2], _cA.buttonDownBg[3], _cA.buttonDownBg[4] or 0.85) + closeBtn:SetBackdropBorderColor(_cA.btnBorder[1], _cA.btnBorder[2], _cA.btnBorder[3], _cA.btnBorder[4] or 0.7) + local closeIco = SFrames:CreateIcon(closeBtn, "close", 14) + closeIco:SetDrawLayer("OVERLAY") + closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) + closeIco:SetVertexColor(1, 0.7, 0.7) + closeBtn:SetScript("OnClick", function() SFrames.ConfigUI.frame:Hide() end) + closeBtn:SetScript("OnEnter", function() + this:SetBackdropColor(_cA.btnHoverBg[1], _cA.btnHoverBg[2], _cA.btnHoverBg[3], _cA.btnHoverBg[4] or 0.95) + this:SetBackdropBorderColor(_cA.btnHoverBd[1], _cA.btnHoverBd[2], _cA.btnHoverBd[3], _cA.btnHoverBd[4] or 0.9) + end) + closeBtn:SetScript("OnLeave", function() + this:SetBackdropColor(_cA.buttonDownBg[1], _cA.buttonDownBg[2], _cA.buttonDownBg[3], _cA.buttonDownBg[4] or 0.85) + this:SetBackdropBorderColor(_cA.btnBorder[1], _cA.btnBorder[2], _cA.btnBorder[3], _cA.btnBorder[4] or 0.7) + end) + + local tabUI = CreateFrame("Button", "SFramesConfigTabUI", panel, "UIPanelButtonTemplate") + tabUI:SetWidth(100) + tabUI:SetHeight(28) + tabUI:SetPoint("TOPLEFT", panel, "TOPLEFT", 10, -50) + tabUI:SetText("界面设置") + tabUI:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("ui") end) + StyleButton(tabUI) + AddBtnIcon(tabUI, "settings", nil, "left") + + local tabPlayer = CreateFrame("Button", "SFramesConfigTabPlayer", panel, "UIPanelButtonTemplate") + tabPlayer:SetWidth(100) + tabPlayer:SetHeight(28) + tabPlayer:SetPoint("TOP", tabUI, "BOTTOM", 0, -4) + tabPlayer:SetText("玩家框体") + tabPlayer:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("player") end) + StyleButton(tabPlayer) + AddBtnIcon(tabPlayer, "character", nil, "left") + + local tabTarget = CreateFrame("Button", "SFramesConfigTabTarget", panel, "UIPanelButtonTemplate") + tabTarget:SetWidth(100) + tabTarget:SetHeight(28) + tabTarget:SetPoint("TOP", tabPlayer, "BOTTOM", 0, -4) + tabTarget:SetText("目标框体") + tabTarget:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("target") end) + StyleButton(tabTarget) + AddBtnIcon(tabTarget, "search", nil, "left") + + local tabParty = CreateFrame("Button", "SFramesConfigTabParty", panel, "UIPanelButtonTemplate") + tabParty:SetWidth(100) + tabParty:SetHeight(28) + tabParty:SetPoint("TOP", tabTarget, "BOTTOM", 0, -4) + tabParty:SetText("小队框架") + tabParty:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("party") end) + StyleButton(tabParty) + AddBtnIcon(tabParty, "friends", nil, "left") + + local tabRaid = CreateFrame("Button", "SFramesConfigTabRaid", panel, "UIPanelButtonTemplate") + tabRaid:SetWidth(100) + tabRaid:SetHeight(28) + tabRaid:SetPoint("TOP", tabParty, "BOTTOM", 0, -4) + tabRaid:SetText("团队框架") + tabRaid:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("raid") end) + StyleButton(tabRaid) + AddBtnIcon(tabRaid, "party", nil, "left") + + local tabBags = CreateFrame("Button", "SFramesConfigTabBags", panel, "UIPanelButtonTemplate") + tabBags:SetWidth(100) + tabBags:SetHeight(28) + tabBags:SetPoint("TOP", tabRaid, "BOTTOM", 0, -4) + tabBags:SetText("背包设置") + tabBags:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("bags") end) + StyleButton(tabBags) + AddBtnIcon(tabBags, "backpack", nil, "left") + + local tabChar = CreateFrame("Button", "SFramesConfigTabChar", panel, "UIPanelButtonTemplate") + tabChar:SetWidth(100) + tabChar:SetHeight(28) + tabChar:SetPoint("TOP", tabBags, "BOTTOM", 0, -4) + tabChar:SetText("人物面板") + tabChar:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("char") end) + StyleButton(tabChar) + AddBtnIcon(tabChar, "charsheet", nil, "left") + + local tabActionBar = CreateFrame("Button", "SFramesConfigTabActionBar", panel, "UIPanelButtonTemplate") + tabActionBar:SetWidth(100) + tabActionBar:SetHeight(28) + tabActionBar:SetPoint("TOP", tabChar, "BOTTOM", 0, -4) + tabActionBar:SetText("动作条") + tabActionBar:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("actionbar") end) + StyleButton(tabActionBar) + AddBtnIcon(tabActionBar, "attack", nil, "left") + + local tabMinimap = CreateFrame("Button", "SFramesConfigTabMinimap", panel, "UIPanelButtonTemplate") + tabMinimap:SetWidth(100) + tabMinimap:SetHeight(28) + tabMinimap:SetPoint("TOP", tabActionBar, "BOTTOM", 0, -4) + tabMinimap:SetText("地图设置") + tabMinimap:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("minimap") end) + StyleButton(tabMinimap) + AddBtnIcon(tabMinimap, "worldmap", nil, "left") + + local tabBuff = CreateFrame("Button", "SFramesConfigTabBuff", panel, "UIPanelButtonTemplate") + tabBuff:SetWidth(100) + tabBuff:SetHeight(28) + tabBuff:SetPoint("TOP", tabMinimap, "BOTTOM", 0, -4) + tabBuff:SetText("Buff栏") + tabBuff:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("buff") end) + StyleButton(tabBuff) + AddBtnIcon(tabBuff, "buff", nil, "left") + + local tabPersonalize = CreateFrame("Button", "SFramesConfigTabPersonalize", panel, "UIPanelButtonTemplate") + tabPersonalize:SetWidth(100) + tabPersonalize:SetHeight(28) + tabPersonalize:SetPoint("TOP", tabBuff, "BOTTOM", 0, -4) + tabPersonalize:SetText("个性化") + tabPersonalize:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("personalize") end) + StyleButton(tabPersonalize) + AddBtnIcon(tabPersonalize, "potion", nil, "left") + + local tabTheme = CreateFrame("Button", "SFramesConfigTabTheme", panel, "UIPanelButtonTemplate") + tabTheme:SetWidth(100) + tabTheme:SetHeight(28) + tabTheme:SetPoint("TOP", tabPersonalize, "BOTTOM", 0, -4) + tabTheme:SetText("主题设置") + tabTheme:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("theme") end) + StyleButton(tabTheme) + AddBtnIcon(tabTheme, "star", nil, "left") + + local content = CreateFrame("Frame", "SFramesConfigContentMain", panel) + content:SetWidth(PANEL_WIDTH - 140) + content:SetHeight(PANEL_HEIGHT - 60) + content:SetPoint("TOPLEFT", panel, "TOPLEFT", 130, -50) + + local uiPage = CreateFrame("Frame", "SFramesConfigUIPage", content) + uiPage:SetAllPoints(content) + + local playerPage = CreateFrame("Frame", "SFramesConfigPlayerPage", content) + playerPage:SetAllPoints(content) + + local targetPage = CreateFrame("Frame", "SFramesConfigTargetPage", content) + targetPage:SetAllPoints(content) + + local bagPage = CreateFrame("Frame", "SFramesConfigBagPage", content) + bagPage:SetAllPoints(content) + + local raidPage = CreateFrame("Frame", "SFramesConfigRaidPage", content) + raidPage:SetAllPoints(content) + + local partyPage = CreateFrame("Frame", "SFramesConfigPartyPage", content) + partyPage:SetAllPoints(content) + + local charPage = CreateFrame("Frame", "SFramesConfigCharPage", content) + charPage:SetAllPoints(content) + + local actionBarPage = CreateFrame("Frame", "SFramesConfigActionBarPage", content) + actionBarPage:SetAllPoints(content) + + local minimapPage = CreateFrame("Frame", "SFramesConfigMinimapPage", content) + minimapPage:SetAllPoints(content) + + local buffPage = CreateFrame("Frame", "SFramesConfigBuffPage", content) + buffPage:SetAllPoints(content) + + local personalizePage = CreateFrame("Frame", "SFramesConfigPersonalizePage", content) + personalizePage:SetAllPoints(content) + + local themePage = CreateFrame("Frame", "SFramesConfigThemePage", content) + themePage:SetAllPoints(content) + + self.frame = panel + self.title = title + self.uiTab = tabUI + self.playerTab = tabPlayer + self.targetTab = tabTarget + self.bagTab = tabBags + self.raidTab = tabRaid + self.partyTab = tabParty + self.charTab = tabChar + self.actionBarTab = tabActionBar + self.minimapTab = tabMinimap + self.buffTab = tabBuff + self.personalizeTab = tabPersonalize + self.themeTab = tabTheme + self.content = content + self.uiPage = uiPage + self.playerPage = playerPage + self.targetPage = targetPage + self.bagPage = bagPage + self.raidPage = raidPage + self.partyPage = partyPage + self.charPage = charPage + self.actionBarPage = actionBarPage + self.minimapPage = minimapPage + self.buffPage = buffPage + self.personalizePage = personalizePage + self.themePage = themePage + + local btnSaveReload = CreateFrame("Button", "SFramesConfigSaveReloadBtn", panel, "UIPanelButtonTemplate") + btnSaveReload:SetWidth(100) + btnSaveReload:SetHeight(26) + btnSaveReload:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -15, 12) + btnSaveReload:SetText("保存并重载") + btnSaveReload:SetScript("OnClick", function() ReloadUI() end) + StyleButton(btnSaveReload) + AddBtnIcon(btnSaveReload, "save") + + local btnConfirm = CreateFrame("Button", "SFramesConfigConfirmBtn", panel, "UIPanelButtonTemplate") + btnConfirm:SetWidth(80) + btnConfirm:SetHeight(26) + btnConfirm:SetPoint("RIGHT", btnSaveReload, "LEFT", -6, 0) + btnConfirm:SetText("确认") + btnConfirm:SetScript("OnClick", function() SFrames.ConfigUI.frame:Hide() end) + StyleButton(btnConfirm) + AddBtnIcon(btnConfirm, "close") + + self._pageBuilt = {} +end + +function SFrames.ConfigUI:BuildPersonalizePage() + local font = SFrames:GetFont() + local page = self.personalizePage + local controls = {} + + local personalizeScroll = CreateScrollArea(page, 4, -4, 548, 458, 620) + local root = personalizeScroll.child + + -- ── 升级训练师提醒 ────────────────────────────────────────── + local trainerSection = CreateSection(root, "升级技能提醒", 8, -8, 520, 100, font) + + table.insert(controls, CreateCheckBox(trainerSection, + "升级时提醒可学习的新技能", 14, -34, + function() return SFramesDB.trainerReminder ~= false end, + function(checked) SFramesDB.trainerReminder = checked end + )) + CreateDesc(trainerSection, "升级后如果有新的职业技能可学习,将在屏幕和聊天窗口显示技能列表提醒", 36, -50, font, 340) + + CreateButton(trainerSection, "模拟提醒", 370, -34, 120, 22, function() + if SFrames.Player and SFrames.Player.ShowTrainerReminder then + local testLevel = UnitLevel("player") + if mod(testLevel, 2) ~= 0 then testLevel = testLevel + 1 end + SFrames.Player:ShowTrainerReminder(testLevel) + end + end) + CreateDesc(trainerSection, "点击按钮预览升级提醒效果", 392, -55, font, 100) + + -- ── 鼠标提示框 ──────────────────────────────────────────────── + local tooltipSection = CreateSection(root, "鼠标提示框", 8, -118, 520, 120, font) + CreateDesc(tooltipSection, "选择游戏提示框的显示位置方式(三选一)", 14, -28, font) + + local function RefreshTooltipMode() + if SFrames.FloatingTooltip and SFrames.FloatingTooltip.ToggleAnchor then + SFrames.FloatingTooltip:ToggleAnchor(SFramesDB.tooltipMode == "CUSTOM") + end + if SFrames.ConfigUI.tooltipCBs then + for _, cb in ipairs(SFrames.ConfigUI.tooltipCBs) do cb:Refresh() end + end + end + SFrames.ConfigUI.tooltipCBs = {} + + local cbDefault = CreateCheckBox(tooltipSection, + "系统默认位置", 14, -46, + function() return (SFramesDB.tooltipMode == "DEFAULT" or SFramesDB.tooltipMode == nil) end, + function(checked) if checked then SFramesDB.tooltipMode = "DEFAULT" RefreshTooltipMode() end end + ) + table.insert(SFrames.ConfigUI.tooltipCBs, cbDefault) + table.insert(controls, cbDefault) + + local cbCursor = CreateCheckBox(tooltipSection, + "跟随鼠标(鼠标右下方)", 14, -72, + function() return SFramesDB.tooltipMode == "CURSOR" end, + function(checked) if checked then SFramesDB.tooltipMode = "CURSOR" RefreshTooltipMode() end end + ) + table.insert(SFrames.ConfigUI.tooltipCBs, cbCursor) + table.insert(controls, cbCursor) + + local cbCustom = CreateCheckBox(tooltipSection, + "自定义固定位置", 270, -46, + function() return SFramesDB.tooltipMode == "CUSTOM" end, + function(checked) if checked then SFramesDB.tooltipMode = "CUSTOM" RefreshTooltipMode() end end + ) + table.insert(SFrames.ConfigUI.tooltipCBs, cbCustom) + table.insert(controls, cbCustom) + CreateDesc(tooltipSection, "启用后屏幕上会出现绿色锚点,拖动到目标位置后锁定", 292, -62, font) + + -- ── AFK 待机动画 ────────────────────────────────────────────── + local afkSection = CreateSection(root, "AFK 待机动画", 8, -248, 520, 146, font) + + table.insert(controls, CreateCheckBox(afkSection, + "启用 AFK 待机画面", 14, -34, + function() return SFramesDB.afkEnabled ~= false end, + function(checked) SFramesDB.afkEnabled = checked end + )) + CreateDesc(afkSection, "进入暂离状态后显示全屏待机画面(角色跳舞 + 信息面板)", 36, -50, font) + + table.insert(controls, CreateCheckBox(afkSection, + "在非休息区也启用", 14, -70, + function() return SFramesDB.afkOutsideRest == true end, + function(checked) SFramesDB.afkOutsideRest = checked end + )) + CreateDesc(afkSection, "默认仅在旅店/主城等休息区触发,勾选后在任何区域都会触发", 36, -86, font) + + table.insert(controls, CreateSlider(afkSection, + "AFK 延迟(分钟)", 14, -116, 200, 0, 15, 1, + function() return SFramesDB.afkDelay or 5 end, + function(v) SFramesDB.afkDelay = v end, + function(v) if v == 0 then return "立即" end return v .. " 分钟" end + )) + CreateDesc(afkSection, "进入暂离后等待多久才显示待机画面", 240, -116, font) + + CreateButton(afkSection, "预览待机画面", 370, -34, 120, 22, function() + if SFrames.AFKScreen and SFrames.AFKScreen.Toggle then + SFrames.AFKScreen._manualTrigger = true + SFrames.AFKScreen:Show() + end + end) + + personalizeScroll:UpdateRange() + self.personalizeControls = controls + self.personalizeScroll = personalizeScroll +end + +function SFrames.ConfigUI:BuildThemePage() + local font = SFrames:GetFont() + local page = self.themePage + local controls = {} + + local themeScroll = CreateScrollArea(page, 4, -4, 548, 458, 680) + local root = themeScroll.child + + --------------------------------------------------------------------------- + -- Section 1: Theme color selection (regular + class presets) + --------------------------------------------------------------------------- + local sec1 = CreateSection(root, "主题色", 8, -8, 520, 340, font) + + CreateDesc(sec1, "选择主题色后界面全局配色将跟随变化,重载 UI 后完全生效。", + 14, -34, font, 480) + + local SWATCH_SIZE = 32 + local SWATCH_GAP = 8 + local SWATCH_ROW = SWATCH_SIZE + SWATCH_GAP + 14 + local presets = SFrames.Theme.Presets + local order = SFrames.Theme.PresetOrder + local swatches = {} + + local function CreateSwatchRow(parent, presetList, startY) + for idx = 1, table.getn(presetList) do + local key = presetList[idx] + local preset = presets[key] + if preset then + local col = (idx - 1) - math.floor((idx - 1) / 5) * 5 + local row = math.floor((idx - 1) / 5) + local sx = 14 + col * (SWATCH_SIZE + SWATCH_GAP) + local sy = startY + row * -SWATCH_ROW + + local swatch = CreateFrame("Button", NextWidgetName("Swatch"), parent) + swatch:SetWidth(SWATCH_SIZE) + swatch:SetHeight(SWATCH_SIZE) + swatch:SetPoint("TOPLEFT", parent, "TOPLEFT", sx, sy) + + swatch:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, edgeSize = 2, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + + local r, g, b + if preset.swatchRGB then + r, g, b = preset.swatchRGB[1], preset.swatchRGB[2], preset.swatchRGB[3] + else + r, g, b = SFrames.Theme.HSVtoRGB(preset.hue, 0.50 * (preset.satMul or 1), 0.75) + end + swatch:SetBackdropColor(r, g, b, 1) + swatch:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) + + local nameFs = swatch:CreateFontString(nil, "OVERLAY") + nameFs:SetFont(font, 8, "OUTLINE") + nameFs:SetPoint("TOP", swatch, "BOTTOM", 0, -1) + nameFs:SetText(preset.name) + nameFs:SetTextColor(0.75, 0.75, 0.75) + + swatch.presetKey = key + swatch.selectFrame = CreateFrame("Frame", nil, swatch) + swatch.selectFrame:SetPoint("TOPLEFT", swatch, "TOPLEFT", -4, 4) + swatch.selectFrame:SetPoint("BOTTOMRIGHT", swatch, "BOTTOMRIGHT", 4, -4) + swatch.selectFrame:SetBackdrop({ + edgeFile = "Interface\\Buttons\\WHITE8X8", + edgeSize = 2, + }) + swatch.selectFrame:SetBackdropBorderColor(1, 0.85, 0, 1) + swatch.selectFrame:Hide() + + swatch.selectGlow = swatch.selectFrame:CreateTexture(nil, "BACKGROUND") + swatch.selectGlow:SetTexture("Interface\\Buttons\\WHITE8X8") + swatch.selectGlow:SetAllPoints(swatch.selectFrame) + swatch.selectGlow:SetVertexColor(1, 0.85, 0, 0.15) + + swatch:SetScript("OnClick", function() + EnsureDB() + SFramesDB.Theme.preset = this.presetKey + SFramesDB.Theme.useClassTheme = false + SFrames.Theme:Apply(this.presetKey) + SFrames.ConfigUI:RefreshThemeSwatches() + SFrames.ConfigUI:RefreshThemePreview() + PlaySound("igMainMenuOptionCheckBoxOn") + end) + swatch:SetScript("OnEnter", function() + this:SetBackdropBorderColor(0.8, 0.8, 0.8, 1) + end) + swatch:SetScript("OnLeave", function() + local active = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.preset + local useClass = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.useClassTheme + if useClass then active = SFrames.Theme:GetCurrentPreset() end + if this.presetKey == active then + this:SetBackdropBorderColor(1, 0.85, 0, 1) + else + this:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) + end + end) + + table.insert(swatches, swatch) + end + end + end + + CreateSwatchRow(sec1, order, -52) + + local classLabel = sec1:CreateFontString(nil, "OVERLAY") + classLabel:SetFont(font, 10, "OUTLINE") + classLabel:SetPoint("TOPLEFT", sec1, "TOPLEFT", 14, -164) + classLabel:SetText("职业色") + classLabel:SetTextColor(0.85, 0.85, 0.85) + + local divLine = sec1:CreateTexture(nil, "ARTWORK") + divLine:SetTexture("Interface\\Buttons\\WHITE8X8") + divLine:SetHeight(1) + divLine:SetPoint("TOPLEFT", classLabel, "BOTTOMLEFT", 0, -3) + divLine:SetPoint("RIGHT", sec1, "RIGHT", -14, 0) + divLine:SetVertexColor(0.35, 0.35, 0.40, 0.6) + + CreateSwatchRow(sec1, SFrames.Theme.ClassPresetOrder, -182) + + local classCheck = CreateCheckBox(sec1, + "跟随当前职业自动切换", 14, -296, + function() return SFramesDB and SFramesDB.Theme and SFramesDB.Theme.useClassTheme end, + function(checked) + EnsureDB() + SFramesDB.Theme.useClassTheme = checked + if checked then + SFrames.Theme:Apply(SFrames.Theme:GetCurrentPreset()) + else + SFrames.Theme:Apply(SFramesDB.Theme.preset or "pink") + end + SFrames.ConfigUI:RefreshThemeSwatches() + SFrames.ConfigUI:RefreshThemePreview() + end + ) + table.insert(controls, classCheck) + + self._themeSwatches = swatches + + --------------------------------------------------------------------------- + -- Section 3: Icon set picker + --------------------------------------------------------------------------- + local sec3 = CreateSection(root, "图标风格", 8, -356, 520, 110, font) + + CreateDesc(sec3, "选择图标风格后需重载 UI 以完全生效。", + 14, -34, font, 480) + + local ICON_SETS = { "icon", "icon2", "icon3", "icon4", "icon5", "icon6", "icon7", "icon8" } + local ICON_SET_SIZE = 48 + local ICON_SET_GAP = 10 + local faction = UnitFactionGroup and UnitFactionGroup("player") or "Alliance" + local factionKey = (faction == "Horde") and "horde" or "alliance" + local factionCoords = SFrames.ICON_TCOORDS and SFrames.ICON_TCOORDS[factionKey] + if not factionCoords then + factionCoords = (factionKey == "horde") + and { 0.75, 0.875, 0, 0.125 } + or { 0.625, 0.75, 0, 0.125 } + end + + local iconSetBtns = {} + for idx = 1, table.getn(ICON_SETS) do + local setKey = ICON_SETS[idx] + local texPath = "Interface\\AddOns\\Nanami-UI\\img\\" .. setKey + local sx = 14 + (idx - 1) * (ICON_SET_SIZE + ICON_SET_GAP) + + local btn = CreateFrame("Button", NextWidgetName("IcoSet"), sec3) + btn:SetWidth(ICON_SET_SIZE) + btn:SetHeight(ICON_SET_SIZE) + btn:SetPoint("TOPLEFT", sec3, "TOPLEFT", sx, -50) + + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + btn:SetBackdropColor(0.08, 0.08, 0.1, 0.85) + btn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + + local preview = btn:CreateTexture(nil, "ARTWORK") + preview:SetTexture(texPath) + preview:SetTexCoord(factionCoords[1], factionCoords[2], factionCoords[3], factionCoords[4]) + preview:SetPoint("CENTER", btn, "CENTER", 0, 0) + preview:SetWidth(ICON_SET_SIZE - 8) + preview:SetHeight(ICON_SET_SIZE - 8) + + btn.setKey = setKey + btn.selectBorder = CreateFrame("Frame", nil, btn) + btn.selectBorder:SetPoint("TOPLEFT", btn, "TOPLEFT", -2, 2) + btn.selectBorder:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 2, -2) + btn.selectBorder:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 2 }) + btn.selectBorder:SetBackdropBorderColor(1, 1, 1, 1) + btn.selectBorder:Hide() + + btn:SetScript("OnClick", function() + EnsureDB() + SFramesDB.Theme.iconSet = this.setKey + SFrames.ConfigUI:RefreshIconSetButtons() + if SFrames.MinimapButton and SFrames.MinimapButton.Refresh then + SFrames.MinimapButton:Refresh() + end + PlaySound("igMainMenuOptionCheckBoxOn") + end) + btn:SetScript("OnEnter", function() + this:SetBackdropBorderColor(0.7, 0.7, 0.7, 1) + GameTooltip:SetOwner(this, "ANCHOR_NONE") + GameTooltip:ClearAllPoints() + GameTooltip:SetPoint("BOTTOM", this, "TOP", 0, 6) + local num = this.setKey == "icon" and "1" or string.sub(this.setKey, 5) + GameTooltip:SetText("图标风格 " .. num .. (this.setKey == "icon" and " (默认)" or "")) + GameTooltip:AddLine("点击选择,重载 UI 后生效", 0.7, 0.7, 0.7) + GameTooltip:Show() + GameTooltip:SetFrameStrata("TOOLTIP") + GameTooltip:Raise() + end) + btn:SetScript("OnLeave", function() + local cur = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.iconSet or "icon" + if this.setKey == cur then + this:SetBackdropBorderColor(1, 0.85, 0.6, 1) + else + this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + end + GameTooltip:Hide() + end) + + table.insert(iconSetBtns, btn) + end + self._iconSetBtns = iconSetBtns + self:RefreshIconSetButtons() + + --------------------------------------------------------------------------- + -- Section 4: Preview + --------------------------------------------------------------------------- + local sec4 = CreateSection(root, "主题预览", 8, -478, 520, 180, font) + + local previewPanel = CreateFrame("Frame", NextWidgetName("Preview"), sec4) + previewPanel:SetWidth(490) + previewPanel:SetHeight(50) + previewPanel:SetPoint("TOPLEFT", sec4, "TOPLEFT", 14, -34) + EnsureSoftBackdrop(previewPanel) + + local previewTitle = previewPanel:CreateFontString(nil, "OVERLAY") + previewTitle:SetFont(font, 12, "OUTLINE") + previewTitle:SetPoint("TOPLEFT", previewPanel, "TOPLEFT", 10, -8) + previewTitle:SetText("面板标题示例") + + local previewText = previewPanel:CreateFontString(nil, "OVERLAY") + previewText:SetFont(font, 10, "OUTLINE") + previewText:SetPoint("TOPLEFT", previewPanel, "TOPLEFT", 10, -26) + previewText:SetText("正文文本 / Body text") + + local previewDim = previewPanel:CreateFontString(nil, "OVERLAY") + previewDim:SetFont(font, 9, "OUTLINE") + previewDim:SetPoint("TOPLEFT", previewPanel, "TOPLEFT", 10, -40) + previewDim:SetText("辅助文字 / Dim text") + + local previewBtn = CreateFrame("Button", NextWidgetName("PreviewBtn"), sec4, "UIPanelButtonTemplate") + previewBtn:SetWidth(90) + previewBtn:SetHeight(24) + previewBtn:SetPoint("TOPLEFT", sec4, "TOPLEFT", 14, -92) + previewBtn:SetText("按钮示例") + StyleButton(previewBtn) + + local previewAccent = sec4:CreateTexture(nil, "ARTWORK") + previewAccent:SetTexture("Interface\\Buttons\\WHITE8X8") + previewAccent:SetWidth(490) + previewAccent:SetHeight(4) + previewAccent:SetPoint("TOPLEFT", sec4, "TOPLEFT", 14, -122) + + local previewSlot = CreateFrame("Frame", NextWidgetName("PreviewSlot"), sec4) + previewSlot:SetWidth(80) + previewSlot:SetHeight(24) + previewSlot:SetPoint("TOPLEFT", sec4, "TOPLEFT", 14, -134) + previewSlot:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + + local previewSlotLabel = previewSlot:CreateFontString(nil, "OVERLAY") + previewSlotLabel:SetFont(font, 9, "OUTLINE") + previewSlotLabel:SetPoint("CENTER", previewSlot, "CENTER", 0, 0) + previewSlotLabel:SetText("色块") + + self._themePreview = { + panel = previewPanel, + title = previewTitle, + text = previewText, + dim = previewDim, + btn = previewBtn, + accent = previewAccent, + slot = previewSlot, + slotLabel = previewSlotLabel, + } + + --------------------------------------------------------------------------- + -- Hint + --------------------------------------------------------------------------- + CreateDesc(root, "提示: 更换主题后建议点击右下方 [保存并重载] 以确保所有界面生效。", + 14, -650, font, 500) + + --------------------------------------------------------------------------- + -- Finalize + --------------------------------------------------------------------------- + themeScroll:UpdateRange() + self.themeControls = controls + self.themeScroll = themeScroll + + self:RefreshThemeSwatches() + self:RefreshThemePreview() +end + +function SFrames.ConfigUI:RefreshThemeSwatches() + if not self._themeSwatches then return end + local active = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.preset or "pink" + local useClass = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.useClassTheme + if useClass then + active = SFrames.Theme:GetCurrentPreset() + end + for _, sw in ipairs(self._themeSwatches) do + if sw.presetKey == active then + sw:SetBackdropBorderColor(1, 0.85, 0, 1) + if sw.selectFrame then sw.selectFrame:Show() end + else + sw:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) + if sw.selectFrame then sw.selectFrame:Hide() end + end + end +end + +function SFrames.ConfigUI:RefreshThemePreview() + local p = self._themePreview + if not p then return end + local A = SFrames.ActiveTheme + + if p.panel and p.panel.SetBackdropColor then + p.panel:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.95) + p.panel:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 0.9) + end + if p.title then p.title:SetTextColor(A.title[1], A.title[2], A.title[3]) end + if p.text then p.text:SetTextColor(A.text[1], A.text[2], A.text[3]) end + if p.dim then p.dim:SetTextColor(A.dimText[1], A.dimText[2], A.dimText[3]) end + if p.accent then p.accent:SetVertexColor(A.accent[1], A.accent[2], A.accent[3], A.accent[4] or 1) end + if p.slot and p.slot.SetBackdropColor then + p.slot:SetBackdropColor(A.slotBg[1], A.slotBg[2], A.slotBg[3], A.slotBg[4] or 0.9) + p.slot:SetBackdropBorderColor(A.slotSelected[1], A.slotSelected[2], A.slotSelected[3], A.slotSelected[4] or 1) + end + if p.slotLabel then p.slotLabel:SetTextColor(A.nameText[1], A.nameText[2], A.nameText[3]) end +end + +function SFrames.ConfigUI:RefreshIconSetButtons() + if not self._iconSetBtns then return end + local cur = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.iconSet or "icon" + for _, btn in ipairs(self._iconSetBtns) do + if btn.setKey == cur then + btn:SetBackdropBorderColor(1, 0.85, 0.6, 1) + if btn.selectBorder then btn.selectBorder:Show() end + else + btn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + if btn.selectBorder then btn.selectBorder:Hide() end + end + end +end + +function SFrames.ConfigUI:EnsurePage(mode) + if self._pageBuilt[mode] then return end + self._pageBuilt[mode] = true + if mode == "ui" then self:BuildUIPage() + elseif mode == "player" then self:BuildPlayerPage() + elseif mode == "target" then self:BuildTargetPage() + elseif mode == "party" then self:BuildPartyPage() + elseif mode == "bags" then self:BuildBagPage() + elseif mode == "raid" then self:BuildRaidPage() + elseif mode == "char" then self:BuildCharPage() + elseif mode == "actionbar" then self:BuildActionBarPage() + elseif mode == "minimap" then self:BuildMinimapPage() + elseif mode == "buff" then self:BuildBuffPage() + elseif mode == "personalize" then self:BuildPersonalizePage() + elseif mode == "theme" then self:BuildThemePage() + end +end + +function SFrames.ConfigUI:Build(mode) + EnsureDB() + self:EnsureFrame() + + local page = string.lower(mode or "ui") + local validPages = { ui = true, player = true, target = true, bags = true, char = true, party = true, raid = true, actionbar = true, minimap = true, buff = true, personalize = true, theme = true } + if not validPages[page] then page = "ui" end + + if self.frame:IsShown() and self.activePage == page then + self.frame:Hide() + return + end + + self:ShowPage(page) + self.frame:Show() + self.frame:Raise() +end + +function SFrames.ConfigUI:OpenUI() + self:Build("ui") +end + +function SFrames.ConfigUI:OpenBags() + self:Build("bags") +end diff --git a/Core.lua b/Core.lua new file mode 100644 index 0000000..bcf2191 --- /dev/null +++ b/Core.lua @@ -0,0 +1,524 @@ +-- S-Frames Core Initialize +SFrames = {} +DEFAULT_CHAT_FRAME:AddMessage("SF: Loading Core.lua...") + +BINDING_HEADER_NANAMI_UI = "Nanami-UI" +BINDING_NAME_NANAMI_TOGGLE_NAV = "切换导航地图" + +SFrames.eventFrame = CreateFrame("Frame", "SFramesEventFrame", UIParent) +SFrames.events = {} + +function SFrames:GetIncomingHeals(unit) + -- Source 1: ShaguTweaks libpredict + if ShaguTweaks and ShaguTweaks.libpredict and ShaguTweaks.libpredict.UnitGetIncomingHeals then + local lp = ShaguTweaks.libpredict + if lp.UnitGetIncomingHealsBreakdown then + local ok, total, mine, others = pcall(function() + return lp:UnitGetIncomingHealsBreakdown(unit, UnitName("player")) + end) + if ok then + return math.max(0, tonumber(total) or 0), + math.max(0, tonumber(mine) or 0), + math.max(0, tonumber(others) or 0) + end + end + local ok, amount = pcall(function() return lp:UnitGetIncomingHeals(unit) end) + if ok then + amount = math.max(0, tonumber(amount) or 0) + return amount, 0, amount + end + end + -- Source 2: HealComm-1.0 (AceLibrary) + if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("HealComm-1.0") then + local ok, HC = pcall(function() return AceLibrary("HealComm-1.0") end) + if ok and HC and HC.getHeal then + local name = UnitName(unit) + if name then + local total = HC:getHeal(name) or 0 + total = math.max(0, tonumber(total) or 0) + return total, 0, total + end + end + end + return 0, 0, 0 +end + +-- Event Dispatcher +SFrames.eventFrame:SetScript("OnEvent", function() + if SFrames.events[event] then + for i, func in ipairs(SFrames.events[event]) do + func(event) + end + end +end) + +function SFrames:RegisterEvent(event, func) + if not self.events[event] then + self.events[event] = {} + self.eventFrame:RegisterEvent(event) + end + table.insert(self.events[event], func) +end + +function SFrames:UnregisterEvent(event, func) + if self.events[event] then + for i, f in ipairs(self.events[event]) do + if f == func then + table.remove(self.events[event], i) + break + end + end + if table.getn(self.events[event]) == 0 then + self.events[event] = nil + self.eventFrame:UnregisterEvent(event) + end + end +end + +-- Print Helper +function SFrames:Print(msg) + local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9" + DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r " .. tostring(msg)) +end + +-- Addon Loaded Initializer +SFrames:RegisterEvent("PLAYER_LOGIN", function() + SFrames:Initialize() +end) + +function SFrames:SafeInit(name, initFn) + local ok, err = pcall(initFn) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: " .. name .. " init failed: " .. tostring(err) .. "|r") + end +end + +function SFrames:Initialize() + if not SFramesDB then SFramesDB = {} end + + if not SFramesDB.setupComplete then + if SFrames.SetupWizard and SFrames.SetupWizard.Show then + SFrames.SetupWizard:Show(function() + SFrames:DoFullInitialize() + end, "firstrun") + else + SFramesDB.setupComplete = true + self:DoFullInitialize() + end + return + end + + self:DoFullInitialize() +end + +function SFrames:DoFullInitialize() + self:Print("Nanami-UI 正在加载,喵呜~ =^_^=") + + self:HideBlizzardFrames() + + SFrames.Tooltip = CreateFrame("GameTooltip", "SFramesScanTooltip", nil, "GameTooltipTemplate") + SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") + + -- Phase 1: Critical modules (unit frames, action bars) — must load immediately + if SFramesDB.enableUnitFrames ~= false then + if SFrames.Player and SFrames.Player.Initialize then SFrames.Player:Initialize() end + if SFrames.Pet and SFrames.Pet.Initialize then SFrames.Pet:Initialize() end + if SFrames.Target and SFrames.Target.Initialize then SFrames.Target:Initialize() end + if SFrames.ToT and SFrames.ToT.Initialize then SFrames.ToT:Initialize() end + if SFrames.Party and SFrames.Party.Initialize then SFrames.Party:Initialize() end + end + if SFrames.FloatingTooltip and SFrames.FloatingTooltip.Initialize then SFrames.FloatingTooltip:Initialize() end + + if SFrames.ActionBars and SFrames.ActionBars.Initialize then + SFrames.ActionBars:Initialize() + end + + self:InitSlashCommands() + + -- Phase 2: Deferred modules — spread across multiple frames to avoid memory spike + local deferred = { + { "Raid", function() if SFramesDB.enableUnitFrames ~= false and SFrames.Raid and SFrames.Raid.Initialize then SFrames.Raid:Initialize() end end }, + { "Bags", function() if SFrames.Bags and SFrames.Bags.Core and SFrames.Bags.Core.Initialize then SFrames.Bags.Core:Initialize() end end }, + { "Focus", function() if SFrames.Focus and SFrames.Focus.Initialize then SFrames.Focus:Initialize() end end }, + { "TalentTree", function() if SFrames.TalentTree and SFrames.TalentTree.Initialize then SFrames.TalentTree:Initialize() end end }, + { "Minimap", function() if SFrames.Minimap and SFrames.Minimap.Initialize then SFrames.Minimap:Initialize() end end }, + { "MinimapBuffs",function() if SFrames.MinimapBuffs and SFrames.MinimapBuffs.Initialize then SFrames.MinimapBuffs:Initialize() end end }, + { "MinimapButton",function() if SFrames.MinimapButton and SFrames.MinimapButton.Initialize then SFrames.MinimapButton:Initialize() end end }, + { "Chat", function() if SFramesDB.enableChat ~= false and SFrames.Chat and SFrames.Chat.Initialize then SFrames.Chat:Initialize() end end }, + { "MapReveal", function() if SFrames.MapReveal and SFrames.MapReveal.Initialize then SFrames.MapReveal:Initialize() end end }, + { "WorldMap", function() if SFrames.WorldMap and SFrames.WorldMap.Initialize then SFrames.WorldMap:Initialize() end end }, + { "MapIcons", function() if SFrames.MapIcons and SFrames.MapIcons.Initialize then SFrames.MapIcons:Initialize() end end }, + { "Tweaks", function() if SFrames.Tweaks and SFrames.Tweaks.Initialize then SFrames.Tweaks:Initialize() end end }, + { "AFKScreen", function() if SFrames.AFKScreen and SFrames.AFKScreen.Initialize then SFrames.AFKScreen:Initialize() end end }, + } + + local idx = 1 + local batchSize = 3 + local deferFrame = CreateFrame("Frame") + deferFrame:SetScript("OnUpdate", function() + if idx > table.getn(deferred) then + this:SetScript("OnUpdate", nil) + SFrames:Print("所有模块加载完成 =^_^=") + return + end + local batchEnd = idx + batchSize - 1 + if batchEnd > table.getn(deferred) then batchEnd = table.getn(deferred) end + for i = idx, batchEnd do + SFrames:SafeInit(deferred[i][1], deferred[i][2]) + end + idx = batchEnd + 1 + end) +end + +function SFrames:GetAuraTimeLeft(unit, index, isBuff) + -- If the unit is the player (e.g. you target yourself), Vanilla API CAN give us exact times + if UnitIsUnit(unit, "player") then + local texture = isBuff and UnitBuff(unit, index) or UnitDebuff(unit, index) + if texture then + local filter = isBuff and "HELPFUL" or "HARMFUL" + for i = 0, 31 do + local buffIndex = GetPlayerBuff(i, filter) + if buffIndex and buffIndex >= 0 then + if GetPlayerBuffTexture(buffIndex) == texture then + local t = GetPlayerBuffTimeLeft(buffIndex) + if t and t > 0 then return t end + end + end + end + end + end + + -- Fallback to ShaguTweaks libdebuff if available (Debuffs only usually) + if ShaguTweaks and ShaguTweaks.libdebuff then + if not isBuff then + local effect, rank, texture, stacks, dtype, duration, libTimeLeft = ShaguTweaks.libdebuff:UnitDebuff(unit, index) + if libTimeLeft and libTimeLeft > 0 then + return libTimeLeft + end + end + end + return 0 +end + +function SFrames:FormatTime(seconds) + if not seconds then return "" end + if seconds >= 3600 then + return math.floor(seconds / 3600) .. "h" + elseif seconds >= 60 then + return math.floor(seconds / 60) .. "m" + else + return math.floor(seconds) .. "s" + end +end + +function SFrames:InitSlashCommands() + DEFAULT_CHAT_FRAME:AddMessage("SF: InitSlashCommands called.") + SLASH_SFRAMES1 = "/nanami" + SLASH_SFRAMES2 = "/nui" + SlashCmdList["SFRAMES"] = function(msg) + local text = msg or "" + text = string.gsub(text, "^%s+", "") + text = string.gsub(text, "%s+$", "") + local _, _, cmd, args = string.find(text, "^(%S+)%s*(.-)$") + cmd = string.lower(cmd or "") + args = args or "" + + if cmd == "unlock" or cmd == "move" then + SFrames:UnlockFrames() + elseif cmd == "lock" then + SFrames:LockFrames() + elseif cmd == "chatreset" then + if SFrames.Chat and SFrames.Chat.ResetPosition then + SFrames.Chat:ResetPosition() + end + elseif cmd == "test" then + if SFrames.Party and SFrames.Party.TestMode then SFrames.Party:TestMode() end + elseif cmd == "partyh" then + if SFrames.Party and SFrames.Party.SetLayout then + SFrames.Party:SetLayout("horizontal") + SFrames:Print("Party layout set to horizontal.") + end + elseif cmd == "partyv" then + if SFrames.Party and SFrames.Party.SetLayout then + SFrames.Party:SetLayout("vertical") + SFrames:Print("Party layout set to vertical.") + end + elseif cmd == "partylayout" or cmd == "playout" then + local mode = string.lower(args or "") + if mode == "h" or mode == "horizontal" then + if SFrames.Party and SFrames.Party.SetLayout then + SFrames.Party:SetLayout("horizontal") + SFrames:Print("Party layout set to horizontal.") + end + elseif mode == "v" or mode == "vertical" then + if SFrames.Party and SFrames.Party.SetLayout then + SFrames.Party:SetLayout("vertical") + SFrames:Print("Party layout set to vertical.") + end + else + local current = (SFramesDB and SFramesDB.partyLayout) or "vertical" + SFrames:Print("Usage: /nui partylayout horizontal|vertical (current: " .. current .. ")") + end + elseif cmd == "focus" then + if not SFrames.Focus then + SFrames:Print("Focus module unavailable.") + return + end + local ok, name, usedNative = SFrames.Focus:SetFromTarget() + if ok then + if usedNative then + SFrames:Print("Focus set: " .. tostring(name) .. " (native)") + else + SFrames:Print("Focus set: " .. tostring(name)) + end + else + SFrames:Print("No valid target to set focus.") + end + elseif cmd == "clearfocus" or cmd == "cf" then + if not SFrames.Focus then + SFrames:Print("Focus module unavailable.") + return + end + SFrames.Focus:Clear() + SFrames:Print("Focus cleared.") + elseif cmd == "targetfocus" or cmd == "tf" then + if not SFrames.Focus then + SFrames:Print("Focus module unavailable.") + return + end + local ok = SFrames.Focus:Target() + if not ok then + SFrames:Print("Focus target not found.") + end + elseif cmd == "fcast" or cmd == "focuscast" then + if not SFrames.Focus then + SFrames:Print("Focus module unavailable.") + return + end + local ok, reason = SFrames.Focus:Cast(args) + if not ok then + if reason == "NO_SPELL" then + SFrames:Print("Usage: /nui fcast ") + elseif reason == "NO_FOCUS" then + SFrames:Print("No focus set.") + elseif reason == "FOCUS_NOT_FOUND" then + SFrames:Print("Focus not found in range/scene.") + else + SFrames:Print("Focus cast failed.") + end + end + elseif cmd == "focushelp" then + SFrames:Print("/nui focus - set current target as focus") + SFrames:Print("/nui clearfocus - clear focus") + SFrames:Print("/nui targetfocus - target focus") + SFrames:Print("/nui fcast - cast spell on focus") + SFrames:Print("/nui partyh / partyv - switch party layout") + SFrames:Print("/nui partylayout h|v - switch party layout") + SFrames:Print("/nui ui - open UI settings") + SFrames:Print("/nui bags - open bag settings") + SFrames:Print("/nui chat - open chat settings panel") + SFrames:Print("/nui chat help - chat command help") + SFrames:Print("/nui chatreset - reset chat frame position") + SFrames:Print("/nui afk - toggle AFK screen") + SFrames:Print("/nui pin - 地图标记 (clear/share)") + SFrames:Print("/nui nav - 切换导航地图") + SFrames:Print("/nui bind - 按键绑定模式(悬停按钮+按键)") + elseif cmd == "ui" or cmd == "uiconfig" then + if SFrames.ConfigUI and SFrames.ConfigUI.Build then SFrames.ConfigUI:Build("ui") end + elseif cmd == "chat" or cmd == "chatconfig" then + if SFrames.Chat and SFrames.Chat.HandleSlash then + SFrames.Chat:HandleSlash(args) + end + elseif cmd == "bags" or cmd == "bag" or cmd == "bagconfig" then + if SFrames.ConfigUI and SFrames.ConfigUI.Build then SFrames.ConfigUI:Build("bags") end + elseif cmd == "mapreveal" or cmd == "mr" then + if SFrames.MapReveal and SFrames.MapReveal.Toggle then + SFrames.MapReveal:Toggle() + else + SFrames:Print("MapReveal module unavailable.") + end + elseif cmd == "stats" or cmd == "stat" or cmd == "ss" then + if SFrames.StatSummary and SFrames.StatSummary.Toggle then + SFrames.StatSummary:Toggle() + else + SFrames:Print("StatSummary module unavailable.") + end + elseif cmd == "afk" then + if SFrames.AFKScreen and SFrames.AFKScreen.Toggle then + SFrames.AFKScreen:Toggle() + else + SFrames:Print("AFKScreen module unavailable.") + end + elseif cmd == "pin" or cmd == "wp" or cmd == "waypoint" then + if not SFrames.WorldMap then + SFrames:Print("WorldMap module unavailable.") + elseif args == "clear" or args == "remove" then + SFrames.WorldMap:ClearWaypoint() + SFrames:Print("地图标记已清除") + elseif args == "share" then + SFrames.WorldMap:ShareWaypoint() + else + SFrames:Print("/nui pin clear - 清除地图标记") + SFrames:Print("/nui pin share - 分享当前标记到聊天") + SFrames:Print("在世界地图中 Ctrl+左键 可放置标记") + end + elseif cmd == "nav" or cmd == "navigation" then + if SFrames.WorldMap and SFrames.WorldMap.ToggleNav then + SFrames.WorldMap:ToggleNav() + else + SFrames:Print("WorldMap module unavailable.") + end + elseif cmd == "bind" or cmd == "keybind" then + if SFrames.ActionBars and SFrames.ActionBars.ToggleKeyBindMode then + SFrames.ActionBars:ToggleKeyBindMode() + else + SFrames:Print("ActionBars module unavailable.") + end + elseif cmd == "config" or cmd == "" then + if SFrames.ConfigUI and SFrames.ConfigUI.Build then SFrames.ConfigUI:Build("ui") end + else + local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9" + DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r Commands: /nui, /nui ui, /nui bags, /nui chat, /nui unlock, /nui lock, /nui test, /nui partyh, /nui partyv, /nui focushelp, /nui mapreveal, /nui stats, /nui afk, /nui pin, /nui bind") + end + end +end + +function SFrames:UnlockFrames() + self.isUnlocked = true + self:Print("Frames Unlocked. Drag to move.") + -- Show overlays or just let them be dragged if they are always movable + if SFramesPlayerFrame then SFramesPlayerFrame:EnableMouse(true) end + if SFramesPetFrame then SFramesPetFrame:EnableMouse(true) end + if SFramesTargetFrame then SFramesTargetFrame:EnableMouse(true) end + if SFrames.Chat and SFrames.Chat.SetUnlocked then + SFrames.Chat:SetUnlocked(true) + end +end + +function SFrames:LockFrames() + self.isUnlocked = false + self:Print("Frames Locked.") + if SFrames.Chat and SFrames.Chat.SetUnlocked then + SFrames.Chat:SetUnlocked(false) + end +end + +function SFrames:HideBlizzardFrames() + -- Hide Character Frame (replaced by CharacterPanel.lua) + -- Only suppress if the custom character panel is enabled + if (not SFramesDB) or (SFramesDB.charPanelEnable ~= false) then + if CharacterFrame then + CharacterFrame:UnregisterAllEvents() + CharacterFrame:Hide() + CharacterFrame.Show = function() end + end + if PaperDollFrame then PaperDollFrame:Hide() end + if ReputationFrame then ReputationFrame:Hide() end + if SkillFrame then SkillFrame:Hide() end + if HonorFrame then HonorFrame:Hide() end + end + + if SFramesDB and SFramesDB.enableUnitFrames == false then + -- Keep Blizzard unit frames when Nanami frames are disabled + else + -- Hide Player Frame + if PlayerFrame then + PlayerFrame:UnregisterAllEvents() + PlayerFrame:Hide() + PlayerFrame.Show = function() end + end + + -- Hide Pet Frame + if PetFrame then + PetFrame:UnregisterAllEvents() + PetFrame:Hide() + PetFrame.Show = function() end + end + + -- Hide Target Frame + if TargetFrame then + TargetFrame:UnregisterAllEvents() + TargetFrame:Hide() + TargetFrame.Show = function() end + end + + -- Hide Combo Frame + if ComboFrame then + ComboFrame:UnregisterAllEvents() + ComboFrame:Hide() + ComboFrame.Show = function() end + end + + -- Hide Party Frames + for i = 1, 4 do + local pf = _G["PartyMemberFrame"..i] + if pf then + pf:UnregisterAllEvents() + pf:Hide() + pf.Show = function() end + end + end + end + + -- Hide Native Raid Frames if SFrames raid is enabled + if (not SFramesDB) or (SFramesDB.enableRaidFrames ~= false) then + + local function NeuterBlizzardRaidUI() + -- Default Classic UI (1.12) + if RaidFrame then + RaidFrame:UnregisterAllEvents() + end + + -- Prevent Raid groups from updating and showing + for i = 1, NUM_RAID_GROUPS or 8 do + local rgf = _G["RaidGroupButton"..i] + if rgf then + rgf:UnregisterAllEvents() + end + end + + -- Override pullout generation + RaidPullout_Update = function() end + RaidPullout_OnEvent = function() end + + -- Hide individual pullout frames that might already exist + for i = 1, 40 do + local pf = _G["RaidPullout"..i] + if pf then + pf:UnregisterAllEvents() + pf:Hide() + pf.Show = function() end + end + end + + -- Hide standard GroupFrames + if RaidGroupFrame_OnEvent then + RaidGroupFrame_OnEvent = function() end + end + + -- Hide newer/backported Compact Raid Frames if they exist + if CompactRaidFrameManager then + CompactRaidFrameManager:UnregisterAllEvents() + CompactRaidFrameManager:Hide() + CompactRaidFrameManager.Show = function() end + end + if CompactRaidFrameContainer then + CompactRaidFrameContainer:UnregisterAllEvents() + CompactRaidFrameContainer:Hide() + CompactRaidFrameContainer.Show = function() end + end + end + + NeuterBlizzardRaidUI() + + -- Hook ADDON_LOADED to catch Blizzard_RaidUI loaded on demand + local raidHook = CreateFrame("Frame") + raidHook:RegisterEvent("ADDON_LOADED") + raidHook:SetScript("OnEvent", function() + if arg1 == "Blizzard_RaidUI" then + NeuterBlizzardRaidUI() + end + end) + end + +end diff --git a/DarkmoonGuide.lua b/DarkmoonGuide.lua new file mode 100644 index 0000000..72d2312 --- /dev/null +++ b/DarkmoonGuide.lua @@ -0,0 +1,363 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Darkmoon Faire Buff Guide (暗月马戏团 Buff 指引) +-- Automatically shows a guide when talking to Sayge, or via /dmf command +-------------------------------------------------------------------------------- + +SFrames.DarkmoonGuide = SFrames.DarkmoonGuide or {} +local DG = SFrames.DarkmoonGuide + +local GUIDE_WIDTH = 420 +local GUIDE_HEIGHT = 520 + +local _A = SFrames.ActiveTheme + +local SAYGE_NAMES = { + ["Sayge"] = true, + ["塞格"] = true, + ["赛格"] = true, + ["Сэйдж"] = true, +} + +-- Buff data: { buff name, q1 answer, q2 answer, description, optional alt path } +local BUFF_DATA = { + { buff = "+10% 伤害", q1 = 1, q2 = 1, star = true, tip = "DPS首选" }, + { buff = "+25 全抗性", q1 = 1, q2 = 2, star = false, tip = "PvP/坦克可选", q1_alt = 2, q2_alt = 3 }, + { buff = "+10% 护甲", q1 = 1, q2 = 3, star = false, tip = "坦克可选", q1_alt = 4, q2_alt = 3 }, + { buff = "+10% 精神", q1 = 2, q2 = 1, star = false, tip = "治疗回蓝", q1_alt = 4, q2_alt = 2 }, + { buff = "+10% 智力", q1 = 2, q2 = 2, star = false, tip = "法系/治疗", q1_alt = 4, q2_alt = 1 }, + { buff = "+10% 耐力", q1 = 3, q2 = 1, star = false, tip = "坦克/PvP" }, + { buff = "+10% 力量", q1 = 3, q2 = 2, star = false, tip = "战士/圣骑" }, + { buff = "+10% 敏捷", q1 = 3, q2 = 3, star = false, tip = "盗贼/猎人" }, +} + +local Q1_LABELS = { + [1] = "第一项: 我会正面迎战", + [2] = "第二项: 我会说服对方", + [3] = "第三项: 我会帮助别人", + [4] = "第四项: 我会独自思考", +} + +local Q2_LABELS = { + [1] = "选第一项", + [2] = "选第二项", + [3] = "选第三项", +} + +-------------------------------------------------------------------------------- +-- Frame creation +-------------------------------------------------------------------------------- +local function CreateGuideFrame() + if DG.frame then return DG.frame end + + local f = CreateFrame("Frame", "NanamiDarkmoonGuide", UIParent) + f:SetWidth(GUIDE_WIDTH) + f:SetHeight(GUIDE_HEIGHT) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 40) + f:SetFrameStrata("FULLSCREEN_DIALOG") + f:SetFrameLevel(500) + f:SetMovable(true) + f:EnableMouse(true) + f:SetClampedToScreen(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + SFrames:CreateRoundBackdrop(f) + local _pbg = _A.panelBg or { 0.06, 0.05, 0.08, 0.96 } + local _pbd = _A.panelBorder or { 0.45, 0.25, 0.55, 1 } + f:SetBackdropColor(_pbg[1], _pbg[2], _pbg[3], _pbg[4]) + f:SetBackdropBorderColor(_pbd[1], _pbd[2], _pbd[3], _pbd[4]) + + table.insert(UISpecialFrames, "NanamiDarkmoonGuide") + + if WorldMapFrame then + local origOnHide = WorldMapFrame:GetScript("OnHide") + WorldMapFrame:SetScript("OnHide", function() + if origOnHide then origOnHide() end + if f:IsShown() then f:Hide() end + end) + end + + -- Title bar + local titleBar = CreateFrame("Frame", nil, f) + titleBar:SetHeight(32) + titleBar:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -8) + titleBar:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8) + titleBar:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 } + }) + local _hbg = _A.headerBg or { 0.12, 0.08, 0.15, 0.9 } + local _hbd = _A.headerBorder or { 0.5, 0.3, 0.6, 0.6 } + titleBar:SetBackdropColor(_hbg[1], _hbg[2], _hbg[3], _hbg[4]) + titleBar:SetBackdropBorderColor(_hbd[1], _hbd[2], _hbd[3], _hbd[4]) + + local titleIcon = titleBar:CreateTexture(nil, "ARTWORK") + titleIcon:SetTexture("Interface\\Icons\\INV_Misc_Orb_02") + titleIcon:SetWidth(20) + titleIcon:SetHeight(20) + titleIcon:SetPoint("LEFT", titleBar, "LEFT", 8, 0) + + local titleText = SFrames:CreateFontString(titleBar, 14, "LEFT") + titleText:SetPoint("LEFT", titleIcon, "RIGHT", 6, 0) + titleText:SetTextColor(_A.title[1], _A.title[2], _A.title[3]) + titleText:SetText("暗月马戏团 Buff 指引") + + -- Close button + local closeBtn = CreateFrame("Button", nil, f) + closeBtn:SetWidth(20) + closeBtn:SetHeight(20) + closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -12, -13) + closeBtn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 } + }) + local _cbg = _A.closeBtnBg or { 0.3, 0.08, 0.08, 0.9 } + local _cbd = _A.closeBtnBorder or { 0.6, 0.2, 0.2, 0.8 } + local _cbgH = _A.closeBtnHoverBg or { 0.5, 0.1, 0.1, 0.95 } + local _cbdH = _A.closeBtnHoverBorder or { 0.8, 0.3, 0.3, 1 } + closeBtn:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4]) + closeBtn:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4]) + + local closeTxt = SFrames:CreateFontString(closeBtn, 12, "CENTER") + closeTxt:SetAllPoints(closeBtn) + closeTxt:SetText("X") + local _closeTxt = _A.accentLight or { 0.9, 0.5, 0.5 } + closeTxt:SetTextColor(_closeTxt[1], _closeTxt[2], _closeTxt[3]) + + closeBtn:SetScript("OnClick", function() f:Hide() end) + closeBtn:SetScript("OnEnter", function() + this:SetBackdropColor(_cbgH[1], _cbgH[2], _cbgH[3], _cbgH[4]) + this:SetBackdropBorderColor(_cbdH[1], _cbdH[2], _cbdH[3], _cbdH[4]) + end) + closeBtn:SetScript("OnLeave", function() + this:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4]) + this:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4]) + end) + + -- NPC info section + local npcSection = CreateFrame("Frame", nil, f) + npcSection:SetHeight(48) + npcSection:SetPoint("TOPLEFT", titleBar, "BOTTOMLEFT", 0, -8) + npcSection:SetPoint("TOPRIGHT", titleBar, "BOTTOMRIGHT", 0, -8) + + local npcLine1 = SFrames:CreateFontString(npcSection, 11, "LEFT") + npcLine1:SetPoint("TOPLEFT", npcSection, "TOPLEFT", 4, 0) + npcLine1:SetPoint("TOPRIGHT", npcSection, "TOPRIGHT", -4, 0) + npcLine1:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3]) + npcLine1:SetText("NPC: |cffffffffSayge (塞格)|r - 暗月马戏团占卜师") + + local npcLine2 = SFrames:CreateFontString(npcSection, 10, "LEFT") + npcLine2:SetPoint("TOPLEFT", npcLine1, "BOTTOMLEFT", 0, -4) + npcLine2:SetPoint("TOPRIGHT", npcLine1, "BOTTOMRIGHT", 0, -4) + npcLine2:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3]) + npcLine2:SetText("与赛格对话选择不同答案可获得不同2小时Buff (表中 X→Y = 第1题选X, 第2题选Y)") + + -- Separator + local sep1 = f:CreateTexture(nil, "ARTWORK") + sep1:SetTexture("Interface\\Buttons\\WHITE8X8") + sep1:SetHeight(1) + sep1:SetPoint("TOPLEFT", npcSection, "BOTTOMLEFT", 0, -4) + sep1:SetPoint("TOPRIGHT", npcSection, "BOTTOMRIGHT", 0, -4) + local _sep = _A.separator or { 0.4, 0.25, 0.5, 0.5 } + sep1:SetVertexColor(_sep[1], _sep[2], _sep[3], _sep[4]) + + -- Column headers + local headerY = -104 + local colBuff = 12 + local colOpt1 = 150 + local colOpt2 = 260 + local colTip = 355 + + local hBuff = SFrames:CreateFontString(f, 11, "LEFT") + hBuff:SetPoint("TOPLEFT", f, "TOPLEFT", colBuff, headerY) + hBuff:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3]) + hBuff:SetText("Buff效果") + + local hOpt1 = SFrames:CreateFontString(f, 11, "LEFT") + hOpt1:SetPoint("TOPLEFT", f, "TOPLEFT", colOpt1, headerY) + hOpt1:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3]) + hOpt1:SetText("选项1") + + local hOpt2 = SFrames:CreateFontString(f, 11, "LEFT") + hOpt2:SetPoint("TOPLEFT", f, "TOPLEFT", colOpt2, headerY) + hOpt2:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3]) + hOpt2:SetText("选项2") + + local hTip = SFrames:CreateFontString(f, 11, "LEFT") + hTip:SetPoint("TOPLEFT", f, "TOPLEFT", colTip, headerY) + hTip:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3]) + hTip:SetText("备注") + + -- Header separator + local sep2 = f:CreateTexture(nil, "ARTWORK") + sep2:SetTexture("Interface\\Buttons\\WHITE8X8") + sep2:SetHeight(1) + sep2:SetPoint("TOPLEFT", f, "TOPLEFT", 10, headerY - 14) + sep2:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, headerY - 14) + local _sep2c = _A.separator or { 0.35, 0.2, 0.45, 0.4 } + sep2:SetVertexColor(_sep2c[1], _sep2c[2], _sep2c[3], _sep2c[4]) + + -- Buff rows + local rowStart = headerY - 22 + local rowHeight = 24 + + for i, data in ipairs(BUFF_DATA) do + local y = rowStart - (i - 1) * rowHeight + + -- Alternate row background + if math.fmod(i, 2) == 0 then + local rowBg = f:CreateTexture(nil, "BACKGROUND") + rowBg:SetTexture("Interface\\Buttons\\WHITE8X8") + rowBg:SetHeight(rowHeight) + rowBg:SetPoint("TOPLEFT", f, "TOPLEFT", 8, y + 4) + rowBg:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, y + 4) + local _rbg = _A.sectionBg or { 0.15, 0.1, 0.2, 0.3 } + rowBg:SetVertexColor(_rbg[1], _rbg[2], _rbg[3], 0.3) + end + + -- Star marker for recommended + if data.star then + local star = SFrames:CreateFontString(f, 10, "LEFT") + star:SetPoint("TOPLEFT", f, "TOPLEFT", colBuff - 10, y) + local _stc = _A.title or { 1.0, 0.84, 0.0 } + star:SetTextColor(_stc[1], _stc[2], _stc[3]) + star:SetText("*") + end + + -- Buff name + local buffName = SFrames:CreateFontString(f, 11, "LEFT") + buffName:SetPoint("TOPLEFT", f, "TOPLEFT", colBuff, y) + if data.star then + local _hl = _A.accentLight or { 0.4, 1.0, 0.4 } + buffName:SetTextColor(_hl[1], _hl[2], _hl[3]) + else + buffName:SetTextColor(_A.text[1], _A.text[2], _A.text[3]) + end + buffName:SetText(data.buff) + + -- Option 1 path (q1→q2) + local opt1Text = SFrames:CreateFontString(f, 11, "CENTER") + opt1Text:SetPoint("TOPLEFT", f, "TOPLEFT", colOpt1, y) + local _o1c = _A.title or { 1, 0.82, 0.5 } + opt1Text:SetTextColor(_o1c[1], _o1c[2], _o1c[3]) + opt1Text:SetText(data.q1 .. " → " .. data.q2) + + -- Option 2 path (alt q1→q2) or "/" + local opt2Text = SFrames:CreateFontString(f, 11, "CENTER") + opt2Text:SetPoint("TOPLEFT", f, "TOPLEFT", colOpt2, y) + if data.q1_alt then + local _o2c = _A.accentDark or { 0.7, 0.82, 1.0 } + opt2Text:SetTextColor(_o2c[1], _o2c[2], _o2c[3]) + opt2Text:SetText(data.q1_alt .. " → " .. data.q2_alt) + else + opt2Text:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3]) + opt2Text:SetText("/") + end + + -- Tip + local tipText = SFrames:CreateFontString(f, 10, "LEFT") + tipText:SetPoint("TOPLEFT", f, "TOPLEFT", colTip, y) + tipText:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3]) + tipText:SetText(data.tip) + end + + -- Separator before Q1 detail + local sep3 = f:CreateTexture(nil, "ARTWORK") + sep3:SetTexture("Interface\\Buttons\\WHITE8X8") + sep3:SetHeight(1) + local detailY = rowStart - table.getn(BUFF_DATA) * rowHeight - 4 + sep3:SetPoint("TOPLEFT", f, "TOPLEFT", 10, detailY) + sep3:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, detailY) + local _sep3c = _A.separator or { 0.4, 0.25, 0.5, 0.5 } + sep3:SetVertexColor(_sep3c[1], _sep3c[2], _sep3c[3], _sep3c[4]) + + -- Q1 dialogue detail header + local q1Header = SFrames:CreateFontString(f, 11, "LEFT") + q1Header:SetPoint("TOPLEFT", f, "TOPLEFT", 12, detailY - 10) + q1Header:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3]) + q1Header:SetText("第一题选项对照:") + + -- Q1 options + local q1y = detailY - 28 + for idx, label in pairs(Q1_LABELS) do + local line = SFrames:CreateFontString(f, 10, "LEFT") + line:SetPoint("TOPLEFT", f, "TOPLEFT", 18, q1y - (idx - 1) * 16) + line:SetTextColor(_A.text[1], _A.text[2], _A.text[3]) + line:SetText("|cffffcc66" .. idx .. ".|r " .. label) + end + + -- Q2 dialogue detail header + local q2HeaderY = q1y - 4 * 16 - 8 + local q2Header = SFrames:CreateFontString(f, 11, "LEFT") + q2Header:SetPoint("TOPLEFT", f, "TOPLEFT", 12, q2HeaderY) + q2Header:SetTextColor(_A.sectionTitle[1], _A.sectionTitle[2], _A.sectionTitle[3]) + q2Header:SetText("第二题: 根据上表 \"X → Y\" 中的Y选对应项 (共3项)") + + -- Tip at bottom + local tipY = q2HeaderY - 24 + local bottomTip = SFrames:CreateFontString(f, 10, "LEFT") + bottomTip:SetPoint("TOPLEFT", f, "TOPLEFT", 12, tipY) + bottomTip:SetPoint("TOPRIGHT", f, "TOPRIGHT", -12, tipY) + local _btc = _A.title or { 1.0, 0.84, 0.0 } + bottomTip:SetTextColor(_btc[1], _btc[2], _btc[3]) + bottomTip:SetText("* 推荐: 大部分职业选 +10% 伤害 (第1题选1, 第2题选1)") + + local bottomTip2 = SFrames:CreateFontString(f, 10, "LEFT") + bottomTip2:SetPoint("TOPLEFT", bottomTip, "BOTTOMLEFT", 0, -6) + bottomTip2:SetPoint("TOPRIGHT", bottomTip, "BOTTOMRIGHT", 0, -6) + bottomTip2:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3]) + bottomTip2:SetText("输入 /dmf 可随时打开此面板 | 可拖动移动 | ESC关闭") + + f:Hide() + DG.frame = f + return f +end + +-------------------------------------------------------------------------------- +-- Toggle +-------------------------------------------------------------------------------- +function DG:Toggle() + local f = CreateGuideFrame() + if f:IsShown() then + f:Hide() + else + f:Show() + end +end + +function DG:Show() + local f = CreateGuideFrame() + f:Show() +end + +function DG:Hide() + if DG.frame then DG.frame:Hide() end +end + +-------------------------------------------------------------------------------- +-- Slash command: /dmf +-------------------------------------------------------------------------------- +SLASH_DARKMOONGUIDE1 = "/dmf" +SLASH_DARKMOONGUIDE2 = "/darkmoon" +SlashCmdList["DARKMOONGUIDE"] = function() + DG:Toggle() +end + +-------------------------------------------------------------------------------- +-- Auto-show when talking to Sayge +-------------------------------------------------------------------------------- +local detector = CreateFrame("Frame", "NanamiDarkmoonDetector", UIParent) +detector:RegisterEvent("GOSSIP_SHOW") +detector:SetScript("OnEvent", function() + local npcName = UnitName("npc") + if npcName and SAYGE_NAMES[npcName] then + DG:Show() + end +end) + +DEFAULT_CHAT_FRAME:AddMessage("SF: Loading DarkmoonGuide.lua...") diff --git a/DarkmoonMapMarker.lua b/DarkmoonMapMarker.lua new file mode 100644 index 0000000..e9ef62a --- /dev/null +++ b/DarkmoonMapMarker.lua @@ -0,0 +1,2 @@ +-- Darkmoon Faire map markers are now integrated into WorldMap.lua (section 8b) +-- This file is intentionally left minimal to avoid duplicate code. diff --git a/Factory.lua b/Factory.lua new file mode 100644 index 0000000..82d6155 --- /dev/null +++ b/Factory.lua @@ -0,0 +1,252 @@ +-- Helper function to generate ElvUI-style backdrop and shadow border +function SFrames:CreateBackdrop(frame) + frame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 } + }) + local A = SFrames.ActiveTheme + if A and A.panelBg then + frame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.9) + frame:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1) + else + frame:SetBackdropColor(0.1, 0.1, 0.1, 0.9) + frame:SetBackdropBorderColor(0, 0, 0, 1) + end +end + +function SFrames:CreateRoundBackdrop(frame) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + local A = SFrames.ActiveTheme + if A and A.panelBg then + frame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.95) + frame:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 0.9) + else + frame:SetBackdropColor(0.08, 0.08, 0.10, 0.95) + frame:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + end +end + +function SFrames:CreateUnitBackdrop(frame) + SFrames:CreateBackdrop(frame) + frame:SetBackdropBorderColor(0, 0, 0, 1) +end + +-- Generator for StatusBars +function SFrames:CreateStatusBar(parent, name) + local bar = CreateFrame("StatusBar", name, parent) + bar:SetStatusBarTexture(SFrames:GetTexture()) + + if (not SFramesDB or SFramesDB.smoothBars ~= false) and SmoothBar then + SmoothBar(bar) + end + + return bar +end + +-- Generator for FontStrings +function SFrames:CreateFontString(parent, size, justifyH) + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont(SFrames:GetFont(), size or 12, SFrames.Media.fontOutline) + fs:SetJustifyH(justifyH or "CENTER") + fs:SetTextColor(1, 1, 1) + return fs +end + +-- Generator for 3D Portraits +function SFrames:CreatePortrait(parent, name) + local portrait = CreateFrame("PlayerModel", name, parent) + return portrait +end + +-------------------------------------------------------------------------------- +-- Class Icon (circular class portraits from UI-Classes-Circles.tga) +-------------------------------------------------------------------------------- +local CLASS_ICON_PATH = "Interface\\AddOns\\Nanami-UI\\img\\UI-Classes-Circles" + +SFrames.CLASS_ICON_TCOORDS = { + ["WARRIOR"] = { 0, 0.25, 0, 0.25 }, + ["MAGE"] = { 0.25, 0.49609375, 0, 0.25 }, + ["ROGUE"] = { 0.49609375, 0.7421875, 0, 0.25 }, + ["DRUID"] = { 0.7421875, 0.98828125, 0, 0.25 }, + ["HUNTER"] = { 0, 0.25, 0.25, 0.5 }, + ["SHAMAN"] = { 0.25, 0.49609375, 0.25, 0.5 }, + ["PRIEST"] = { 0.49609375, 0.7421875, 0.25, 0.5 }, + ["WARLOCK"] = { 0.7421875, 0.98828125, 0.25, 0.5 }, + ["PALADIN"] = { 0, 0.25, 0.5, 0.75 }, +} + +function SFrames:CreateClassIcon(parent, size) + local sz = size or 20 + local overlay = CreateFrame("Frame", nil, parent) + overlay:SetFrameLevel((parent:GetFrameLevel() or 0) + 3) + overlay:SetWidth(sz) + overlay:SetHeight(sz) + + local icon = overlay:CreateTexture(nil, "OVERLAY") + icon:SetTexture(CLASS_ICON_PATH) + icon:SetAllPoints(overlay) + icon:Hide() + + icon.overlay = overlay + return icon +end + +function SFrames:SetClassIcon(icon, class) + if not icon then return end + local coords = self.CLASS_ICON_TCOORDS[class] + if coords then + icon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]) + icon:Show() + if icon.overlay then icon.overlay:Show() end + else + icon:Hide() + if icon.overlay then icon.overlay:Hide() end + end +end + +-------------------------------------------------------------------------------- +-- UI Icon helpers (icon.tga sprite sheet) +-- SFrames.ICON_PATH and SFrames.ICON_TCOORDS are defined in IconMap.lua +-------------------------------------------------------------------------------- + +local ICON_SET_VALID = { + ["icon"] = true, ["icon2"] = true, ["icon3"] = true, ["icon4"] = true, + ["icon5"] = true, ["icon6"] = true, ["icon7"] = true, ["icon8"] = true, +} + +local function GetIconPath() + local set = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.iconSet + if set and ICON_SET_VALID[set] then + return "Interface\\AddOns\\Nanami-UI\\img\\" .. set + end + return "Interface\\AddOns\\Nanami-UI\\img\\icon" +end + +local ICON_TEX = GetIconPath() + +local ICON_TC_FALLBACK = { + ["logo"] = { 0, 0.125, 0, 0.125 }, + ["save"] = { 0.125, 0.25, 0, 0.125 }, + ["close"] = { 0.25, 0.375, 0, 0.125 }, + ["offline"] = { 0.375, 0.5, 0, 0.125 }, + ["chat"] = { 0, 0.125, 0.125, 0.25 }, + ["settings"] = { 0.125, 0.25, 0.125, 0.25 }, + ["ai"] = { 0.25, 0.375, 0.125, 0.25 }, + ["backpack"] = { 0.375, 0.5, 0.125, 0.25 }, + ["exit"] = { 0, 0.125, 0.25, 0.375 }, + ["party"] = { 0.125, 0.25, 0.25, 0.375 }, + ["loot"] = { 0.25, 0.375, 0.25, 0.375 }, + ["dragon"] = { 0.375, 0.5, 0.25, 0.375 }, + ["casting"] = { 0, 0.125, 0.375, 0.5 }, + ["attack"] = { 0.125, 0.25, 0.375, 0.5 }, + ["damage"] = { 0.25, 0.375, 0.375, 0.5 }, + ["latency"] = { 0.375, 0.5, 0.375, 0.5 }, + ["admin"] = { 0, 0.125, 0.125, 0.25 }, + ["bank"] = { 0.25, 0.375, 0.25, 0.375 }, + ["perf"] = { 0.25, 0.375, 0.375, 0.5 }, +} + +local ICON_TC = SFrames.ICON_TCOORDS or ICON_TC_FALLBACK + +function SFrames:CreateIcon(parent, iconKey, size) + local sz = size or 16 + local tex = parent:CreateTexture(nil, "ARTWORK") + tex:SetTexture(GetIconPath()) + tex:SetWidth(sz) + tex:SetHeight(sz) + + local coords = ICON_TC[iconKey] + if not coords and self.ICON_TCOORDS then + coords = self.ICON_TCOORDS[iconKey] + end + if coords then + tex:SetTexCoord(coords[1], coords[2], coords[3], coords[4]) + end + return tex +end + +function SFrames:SetIcon(tex, iconKey) + if not tex then return end + tex:SetTexture(GetIconPath()) + local coords = ICON_TC[iconKey] + if not coords and self.ICON_TCOORDS then + coords = self.ICON_TCOORDS[iconKey] + end + if coords then + tex:SetTexCoord(coords[1], coords[2], coords[3], coords[4]) + tex:Show() + else + tex:Hide() + end +end + +function SFrames:CreateIconButton(parent, iconKey, iconSize, label, width, height, onClick) + local A = SFrames.ActiveTheme + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(width or 100) + btn:SetHeight(height or 24) + + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 } + }) + + local bgC = (A and A.panelBg) or { 0.12, 0.06, 0.10, 0.9 } + local bdC = (A and A.sepColor) or { 0.45, 0.25, 0.35, 0.8 } + local hvC = (A and A.buttonHoverBg) or { 0.22, 0.12, 0.18, 0.95 } + local hvB = (A and A.accent) or { 0.70, 0.40, 0.55, 1 } + local dnC = (A and A.buttonDownBg) or { 0.08, 0.04, 0.06, 0.95 } + local txC = (A and A.buttonText) or { 0.90, 0.76, 0.84 } + local txH = (A and A.buttonActiveText) or { 1, 0.92, 0.96 } + + btn:SetBackdropColor(bgC[1], bgC[2], bgC[3], bgC[4] or 0.9) + btn:SetBackdropBorderColor(bdC[1], bdC[2], bdC[3], bdC[4] or 0.8) + + local iSz = iconSize or 14 + local ico = self:CreateIcon(btn, iconKey, iSz) + ico:SetPoint("LEFT", btn, "LEFT", 6, 0) + btn.icon = ico + + if label and label ~= "" then + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(self:GetFont(), 10, self.Media.fontOutline or "OUTLINE") + fs:SetPoint("LEFT", ico, "RIGHT", 4, 0) + fs:SetPoint("RIGHT", btn, "RIGHT", -6, 0) + fs:SetJustifyH("LEFT") + fs:SetTextColor(txC[1], txC[2], txC[3]) + fs:SetText(label) + btn.label = fs + end + + btn:SetScript("OnEnter", function() + this:SetBackdropColor(hvC[1], hvC[2], hvC[3], hvC[4] or 0.95) + this:SetBackdropBorderColor(hvB[1], hvB[2], hvB[3], hvB[4] or 1) + if this.label then this.label:SetTextColor(txH[1], txH[2], txH[3]) end + end) + btn:SetScript("OnLeave", function() + this:SetBackdropColor(bgC[1], bgC[2], bgC[3], bgC[4] or 0.9) + this:SetBackdropBorderColor(bdC[1], bdC[2], bdC[3], bdC[4] or 0.8) + if this.label then this.label:SetTextColor(txC[1], txC[2], txC[3]) end + end) + btn:SetScript("OnMouseDown", function() + this:SetBackdropColor(dnC[1], dnC[2], dnC[3], dnC[4] or 0.95) + end) + btn:SetScript("OnMouseUp", function() + this:SetBackdropColor(hvC[1], hvC[2], hvC[3], hvC[4] or 0.95) + end) + + if onClick then + btn:SetScript("OnClick", onClick) + end + + return btn +end diff --git a/FlightData.lua b/FlightData.lua new file mode 100644 index 0000000..fb8cbe5 --- /dev/null +++ b/FlightData.lua @@ -0,0 +1,1012 @@ +FTCData = { + Alliance = { + ['31353121'] = { + ['39064586'] = 648, + ['39622759'] = 227, + ['41614419'] = 551, + ['42778635'] = 468, + ['46258175'] = 500, + ['48257559'] = 155, + ['55288457'] = 619, + ['60413265'] = 326, + ['61013311'] = 548, + ['63626050'] = 314, + ['64553982'] = 748, + }, + ['39064586'] = { + ['31353121'] = 360, + ['39622759'] = 126, + ['41614419'] = 261, + ['42778635'] = 177, + ['46258175'] = 154, + ['63626050'] = 434, + }, + ['39622759'] = { + ['31353121'] = 232, + ['39064586'] = 120, + ['41614419'] = 367, + ['42778635'] = 282, + ['46258175'] = 273, + ['48257559'] = 472, + ['60413265'] = 464, + ['60571116'] = 422, + ['63626050'] = 308, + }, + ['40741020'] = { + ['43250447'] = 86, + ['43367666'] = 185, + ['49090653'] = 414, + ['50797998'] = 331, + ['50856858'] = 282, + ['51285266'] = 97, + ['55734276'] = 130, + ['58939349'] = 389, + ['61259531'] = 186, + }, + ['41614419'] = { + ['31353121'] = 557, + ['39064586'] = 267, + ['39622759'] = 376, + ['41897988'] = 714, + ['42778635'] = 86, + ['46258175'] = 261, + ['48257559'] = 711, + ['49764585'] = 797, + ['53079825'] = 274, + ['55288457'] = 236, + ['60413265'] = 774, + ['60571116'] = 519, + ['61013311'] = 385, + ['63626050'] = 617, + ['64553982'] = 365, + }, + ['41897988'] = { + ['31353121'] = 175, + ['41614419'] = 726, + ['48257559'] = 329, + ['49764585'] = 92, + ['53079825'] = 831, + ['61013311'] = 576, + ['63626050'] = 342, + }, + ['42778635'] = { + ['31353121'] = 473, + ['39064586'] = 181, + ['39622759'] = 291, + ['41614419'] = 84, + ['46258175'] = 176, + ['53079825'] = 190, + ['55288457'] = 151, + ['60413265'] = 689, + ['60571116'] = 435, + ['61013311'] = 301, + ['63626050'] = 675, + ['64553982'] = 281, + }, + ['43250447'] = { + ['40741020'] = 78, + ['43367666'] = 245, + ['47861999'] = 443, + ['49090653'] = 343, + ['50797998'] = 259, + ['50856858'] = 247, + ['51285266'] = 116, + ['52058124'] = 506, + ['54685318'] = 508, + ['55734276'] = 113, + ['57035875'] = 450, + ['58060050'] = 157, + ['58939349'] = 317, + ['61259531'] = 176, + ['69999539'] = 563, + }, + ['43367666'] = { + ['40741020'] = 181, + ['43250447'] = 220, + ['47861999'] = 649, + ['49090653'] = 548, + ['50797998'] = 464, + ['51285266'] = 175, + ['52058124'] = 712, + ['54685318'] = 714, + ['55734276'] = 230, + ['57035875'] = 655, + ['58060050'] = 291, + ['58939349'] = 523, + ['61259531'] = 266, + }, + ['46258175'] = { + ['31353121'] = 511, + ['39064586'] = 153, + ['39622759'] = 279, + ['41614419'] = 231, + ['42778635'] = 148, + ['53079825'] = 337, + ['60413265'] = 538, + ['60571116'] = 283, + ['61013311'] = 150, + ['63626050'] = 381, + }, + ['47861999'] = { + ['40741020'] = 430, + ['43250447'] = 367, + ['43367666'] = 597, + ['49090653'] = 110, + ['50797998'] = 206, + ['50856858'] = 243, + ['51285266'] = 468, + ['52058124'] = 81, + ['54685318'] = 71, + ['57035875'] = 74, + ['58939349'] = 244, + ['69999539'] = 219, + }, + ['48257559'] = { + ['31353121'] = 179, + ['39622759'] = 405, + ['41614419'] = 729, + ['49764585'] = 274, + ['60413265'] = 171, + ['60571116'] = 274, + ['63626050'] = 159, + }, + ['49090653'] = { + ['40741020'] = 324, + ['43250447'] = 261, + ['43367666'] = 490, + ['47861999'] = 107, + ['50797998'] = 89, + ['50856858'] = 135, + ['51285266'] = 309, + ['52058124'] = 186, + ['54685318'] = 176, + ['55734276'] = 250, + ['57035875'] = 113, + ['58060050'] = 221, + ['58939349'] = 163, + ['61259531'] = 423, + ['69999539'] = 324, + }, + ['49764585'] = { + ['31353121'] = 258, + ['41614419'] = 809, + ['41897988'] = 94, + ['46258175'] = 856, + ['53079825'] = 774, + ['60413265'] = 104, + ['60571116'] = 364, + ['63626050'] = 257, + ['64553982'] = 670, + }, + ['50797998'] = { + ['40741020'] = 274, + ['43250447'] = 210, + ['43367666'] = 440, + ['47861999'] = 265, + ['49090653'] = 128, + ['50856858'] = 87, + ['51285266'] = 260, + ['52058124'] = 294, + ['54685318'] = 298, + ['55734276'] = 201, + ['57035875'] = 253, + ['58060050'] = 173, + ['58939349'] = 101, + ['61259531'] = 373, + ['69999539'] = 349, + }, + ['50856858'] = { + ['47861999'] = 280, + ['49090653'] = 178, + ['50797998'] = 94, + ['52058124'] = 342, + ['58060050'] = 96, + ['58939349'] = 152, + ['61259531'] = 265, + ['69999539'] = 398, + }, + ['51285266'] = { + ['40741020'] = 93, + ['43250447'] = 88, + ['43367666'] = 171, + ['47861999'] = 517, + ['49090653'] = 417, + ['50797998'] = 333, + ['50856858'] = 212, + ['54685318'] = 582, + ['55734276'] = 60, + ['57035875'] = 524, + ['58939349'] = 391, + ['61259531'] = 97, + }, + ['52058124'] = { + ['40741020'] = 495, + ['43250447'] = 432, + ['43367666'] = 662, + ['47861999'] = 85, + ['49090653'] = 193, + ['50797998'] = 261, + ['50856858'] = 309, + ['51285266'] = 535, + ['54685318'] = 66, + ['57035875'] = 138, + ['58060050'] = 395, + ['69999539'] = 147, + }, + ['53079825'] = { + ['31353121'] = 660, + ['39064586'] = 370, + ['39622759'] = 478, + ['41614419'] = 272, + ['42778635'] = 188, + ['46258175'] = 363, + ['49764585'] = 776, + ['55288457'] = 68, + ['60413265'] = 671, + ['61013311'] = 282, + ['64553982'] = 121, + }, + ['54685318'] = { + ['40741020'] = 492, + ['43250447'] = 429, + ['43367666'] = 658, + ['47861999'] = 68, + ['49090653'] = 176, + ['50797998'] = 256, + ['51285266'] = 531, + ['52058124'] = 54, + ['57035875'] = 75, + ['58939349'] = 245, + ['61259531'] = 591, + ['69999539'] = 164, + }, + ['55288457'] = { + ['31353121'] = 614, + ['39622759'] = 433, + ['41614419'] = 226, + ['41897988'] = 771, + ['42778635'] = 142, + ['46258175'] = 318, + ['53079825'] = 61, + ['60413265'] = 694, + ['61013311'] = 305, + ['63626050'] = 537, + ['64553982'] = 131, + }, + ['55734276'] = { + ['40741020'] = 133, + ['43250447'] = 113, + ['43367666'] = 227, + ['47861999'] = 422, + ['49090653'] = 441, + ['50797998'] = 357, + ['50856858'] = 153, + ['51285266'] = 60, + ['58060050'] = 61, + ['58939349'] = 415, + ['69999539'] = 540, + }, + ['57035875'] = { + ['43250447'] = 385, + ['43367666'] = 614, + ['47861999'] = 86, + ['49090653'] = 126, + ['50797998'] = 271, + ['51285266'] = 485, + ['52058124'] = 122, + ['54685318'] = 72, + ['58939349'] = 171, + ['61259531'] = 547, + ['69999539'] = 233, + }, + ['58060050'] = { + ['40741020'] = 195, + ['43250447'] = 151, + ['43367666'] = 288, + ['49090653'] = 270, + ['50797998'] = 187, + ['50856858'] = 104, + ['51285266'] = 121, + ['52058124'] = 435, + ['54685318'] = 436, + ['55734276'] = 64, + ['57035875'] = 378, + ['58939349'] = 245, + ['61259531'] = 210, + ['69999539'] = 491, + }, + ['58939349'] = { + ['40741020'] = 342, + ['43250447'] = 279, + ['43367666'] = 508, + ['47861999'] = 250, + ['49090653'] = 153, + ['50797998'] = 109, + ['50856858'] = 152, + ['51285266'] = 326, + ['52058124'] = 285, + ['55734276'] = 267, + ['57035875'] = 164, + ['58060050'] = 239, + ['61259531'] = 441, + }, + ['60413265'] = { + ['31353121'] = 354, + ['39064586'] = 692, + ['39622759'] = 480, + ['41614419'] = 772, + ['41897988'] = 197, + ['46258175'] = 540, + ['48257559'] = 177, + ['49764585'] = 104, + ['53079825'] = 670, + ['60571116'] = 262, + ['61013311'] = 388, + ['63626050'] = 154, + ['64553982'] = 566, + }, + ['60571116'] = { + ['31353121'] = 446, + ['39064586'] = 437, + ['39622759'] = 439, + ['41614419'] = 805, + ['41897988'] = 459, + ['46258175'] = 284, + ['48257559'] = 268, + ['49764585'] = 366, + ['53079825'] = 415, + ['60413265'] = 261, + ['61013311'] = 132, + ['63626050'] = 106, + ['64553982'] = 310, + }, + ['61013311'] = { + ['41897988'] = 588, + ['42778635'] = 301, + ['46258175'] = 153, + ['49764585'] = 494, + ['53079825'] = 283, + ['60413265'] = 391, + ['60571116'] = 135, + ['63626050'] = 241, + ['64553982'] = 178, + }, + ['61259531'] = { + ['43250447'] = 189, + ['43367666'] = 260, + ['49090653'] = 467, + ['50797998'] = 382, + ['50856858'] = 300, + ['51285266'] = 91, + ['52058124'] = 631, + ['55734276'] = 150, + ['58060050'] = 207, + ['58939349'] = 483, + ['69999539'] = 687, + }, + ['63626050'] = { + ['31353121'] = 341, + ['39064586'] = 801, + ['39622759'] = 334, + ['41614419'] = 619, + ['41897988'] = 354, + ['42778635'] = 620, + ['46258175'] = 387, + ['48257559'] = 162, + ['49764585'] = 261, + ['53079825'] = 518, + ['55288457'] = 535, + ['60413265'] = 157, + ['60571116'] = 115, + ['61013311'] = 235, + ['64553982'] = 414, + }, + ['64553982'] = { + ['31353121'] = 734, + ['39064586'] = 444, + ['39622759'] = 553, + ['41614419'] = 346, + ['42778635'] = 262, + ['46258175'] = 327, + ['48257559'] = 572, + ['53079825'] = 122, + ['55288457'] = 122, + ['60413265'] = 564, + ['60571116'] = 309, + ['61013311'] = 176, + ['63626050'] = 408, + }, + ['69999539'] = { + ['43250447'] = 542, + ['47861999'] = 226, + ['49090653'] = 333, + ['50797998'] = 369, + ['50856858'] = 417, + ['51285266'] = 591, + ['52058124'] = 150, + ['54685318'] = 163, + ['58060050'] = 503, + ['61259531'] = 704, + }, + }, + Horde = { + ['31660270'] = { + ['40795725'] = 199, + ['40973830'] = 566, + ['41632127'] = 326, + ['44250988'] = 196, + ['44947779'] = 178, + ['46455299'] = 590, + ['49764585'] = 417, + ['52804720'] = 265, + ['53793668'] = 709, + ['54988908'] = 382, + ['55441868'] = 474, + ['55735743'] = 337, + ['56746834'] = 416, + ['60571116'] = 388, + ['60601317'] = 395, + ['62800776'] = 385, + ['63107562'] = 447, + ['64014458'] = 576, + }, + ['38447540'] = { + ['43159055'] = 863, + ['44267696'] = 112, + ['44825941'] = 782, + ['49442172'] = 95, + ['50543880'] = 526, + ['55498695'] = 471, + ['55533051'] = 556, + ['60541588'] = 751, + ['61540073'] = 212, + ['67153739'] = 289, + ['69752216'] = 299, + }, + ['40795725'] = { + ['31660270'] = 143, + ['40973830'] = 380, + ['41632127'] = 469, + ['44250988'] = 339, + ['44947779'] = 175, + ['46455299'] = 403, + ['52804720'] = 240, + ['54988908'] = 333, + ['55441868'] = 311, + ['55735743'] = 150, + ['56746834'] = 312, + ['60571116'] = 200, + ['60601317'] = 426, + ['62800776'] = 290, + ['63107562'] = 318, + ['64014458'] = 447, + }, + ['40973830'] = { + ['40795725'] = 378, + ['44947779'] = 410, + ['46455299'] = 481, + ['49764585'] = 611, + ['52804720'] = 318, + ['54988908'] = 412, + ['55441868'] = 167, + ['55735743'] = 228, + ['56746834'] = 391, + ['60571116'] = 279, + ['60601317'] = 504, + ['62800776'] = 256, + ['63107562'] = 256, + ['64014458'] = 384, + }, + ['41632127'] = { + ['31660270'] = 330, + ['44250988'] = 129, + ['44947779'] = 389, + ['49764585'] = 97, + ['52804720'] = 373, + ['53793668'] = 768, + ['54988908'] = 236, + ['55735743'] = 394, + ['56746834'] = 425, + ['60571116'] = 445, + ['60601317'] = 241, + ['62800776'] = 535, + ['63107562'] = 562, + }, + ['43159055'] = { + ['38447540'] = 882, + ['44267696'] = 903, + ['44825941'] = 102, + ['49442172'] = 783, + ['50543880'] = 462, + ['55498695'] = 406, + ['55533051'] = 464, + ['60541588'] = 267, + ['61540073'] = 668, + ['67153739'] = 757, + ['69752216'] = 896, + }, + ['44250988'] = { + ['31660270'] = 201, + ['40795725'] = 400, + ['40973830'] = 494, + ['41632127'] = 130, + ['44947779'] = 259, + ['46455299'] = 517, + ['49764585'] = 222, + ['52804720'] = 244, + ['54988908'] = 107, + ['55441868'] = 426, + ['55735743'] = 263, + ['56746834'] = 421, + ['60571116'] = 315, + ['60601317'] = 201, + ['62800776'] = 406, + ['63107562'] = 432, + }, + ['44267696'] = { + ['38447540'] = 106, + ['43159055'] = 880, + ['44825941'] = 800, + ['49442172'] = 141, + ['50543880'] = 543, + ['55498695'] = 488, + ['55533051'] = 573, + ['60541588'] = 768, + ['61540073'] = 301, + ['67153739'] = 284, + ['69752216'] = 261, + }, + ['44825941'] = { + ['38447540'] = 802, + ['43159055'] = 81, + ['44267696'] = 823, + ['49442172'] = 704, + ['50543880'] = 382, + ['55498695'] = 327, + ['55533051'] = 402, + ['60541588'] = 205, + ['61540073'] = 588, + ['67153739'] = 677, + ['69752216'] = 816, + }, + ['44947779'] = { + ['31660270'] = 159, + ['40795725'] = 182, + ['40973830'] = 389, + ['41632127'] = 381, + ['44250988'] = 252, + ['46455299'] = 411, + ['49764585'] = 397, + ['52804720'] = 87, + ['53793668'] = 532, + ['54988908'] = 204, + ['55441868'] = 296, + ['55735743'] = 159, + ['56746834'] = 239, + ['60571116'] = 210, + ['60601317'] = 290, + ['62800776'] = 207, + ['63107562'] = 269, + ['64014458'] = 398, + }, + ['46455299'] = { + ['31660270'] = 533, + ['40795725'] = 390, + ['40973830'] = 471, + ['41632127'] = 623, + ['44250988'] = 493, + ['44947779'] = 423, + ['49764585'] = 625, + ['52804720'] = 331, + ['53793668'] = 166, + ['55441868'] = 333, + ['55735743'] = 241, + ['56746834'] = 404, + ['60571116'] = 292, + ['60601317'] = 518, + ['62800776'] = 259, + ['63107562'] = 241, + ['64014458'] = 190, + }, + ['49442172'] = { + ['38447540'] = 99, + ['43159055'] = 768, + ['44267696'] = 139, + ['44825941'] = 688, + ['50543880'] = 431, + ['55498695'] = 376, + ['55533051'] = 462, + ['60541588'] = 656, + ['61540073'] = 118, + ['67153739'] = 195, + ['69752216'] = 329, + }, + ['49764585'] = { + ['31660270'] = 424, + ['40795725'] = 542, + ['41632127'] = 100, + ['44250988'] = 224, + ['44947779'] = 416, + ['46455299'] = 645, + ['52804720'] = 336, + ['53793668'] = 766, + ['54988908'] = 200, + ['55441868'] = 551, + ['55735743'] = 392, + ['56746834'] = 333, + ['60571116'] = 443, + ['60601317'] = 113, + ['62800776'] = 462, + ['63107562'] = 559, + ['64014458'] = 689, + }, + ['50543880'] = { + ['38447540'] = 545, + ['43159055'] = 462, + ['44267696'] = 566, + ['44825941'] = 382, + ['49442172'] = 447, + ['55498695'] = 70, + ['55533051'] = 77, + ['60541588'] = 350, + ['61540073'] = 331, + ['67153739'] = 420, + ['69752216'] = 559, + }, + ['52804720'] = { + ['31660270'] = 273, + ['40795725'] = 229, + ['41632127'] = 378, + ['44250988'] = 248, + ['44947779'] = 114, + ['49764585'] = 325, + ['54988908'] = 125, + ['55441868'] = 242, + ['55735743'] = 79, + ['56746834'] = 242, + ['60571116'] = 130, + ['60601317'] = 218, + ['62800776'] = 221, + }, + ['53793668'] = { + ['31660270'] = 645, + ['40795725'] = 502, + ['40973830'] = 528, + ['41632127'] = 734, + ['44250988'] = 604, + ['44947779'] = 532, + ['46455299'] = 157, + ['49764585'] = 736, + ['52804720'] = 443, + ['54988908'] = 537, + ['55441868'] = 369, + ['55735743'] = 353, + ['56746834'] = 515, + ['60571116'] = 404, + ['60601317'] = 629, + ['62800776'] = 370, + ['63107562'] = 275, + ['64014458'] = 142, + }, + ['54988908'] = { + ['31660270'] = 323, + ['40795725'] = 343, + ['40973830'] = 424, + ['41632127'] = 252, + ['44250988'] = 124, + ['44947779'] = 225, + ['49764585'] = 200, + ['52804720'] = 137, + ['55441868'] = 356, + ['55735743'] = 194, + ['56746834'] = 315, + ['60571116'] = 245, + ['60601317'] = 93, + ['62800776'] = 335, + ['63107562'] = 362, + }, + ['55441868'] = { + ['31660270'] = 453, + ['40795725'] = 310, + ['40973830'] = 166, + ['41632127'] = 541, + ['44250988'] = 412, + ['44947779'] = 321, + ['46455299'] = 327, + ['49764585'] = 544, + ['52804720'] = 250, + ['53793668'] = 356, + ['54988908'] = 345, + ['55735743'] = 160, + ['56746834'] = 323, + ['60571116'] = 212, + ['60601317'] = 436, + ['62800776'] = 96, + ['63107562'] = 96, + ['64014458'] = 224, + }, + ['55498695'] = { + ['38447540'] = 477, + ['43159055'] = 417, + ['44267696'] = 497, + ['44825941'] = 313, + ['49442172'] = 379, + ['50543880'] = 56, + ['55533051'] = 87, + ['60541588'] = 280, + ['61540073'] = 263, + ['67153739'] = 352, + ['69752216'] = 491, + }, + ['55533051'] = { + ['38447540'] = 576, + ['43159055'] = 472, + ['44267696'] = 597, + ['44825941'] = 401, + ['49442172'] = 477, + ['50543880'] = 72, + ['55498695'] = 99, + ['60541588'] = 213, + ['61540073'] = 361, + ['67153739'] = 451, + ['69752216'] = 589, + }, + ['55735743'] = { + ['31660270'] = 292, + ['40795725'] = 150, + ['40973830'] = 231, + ['41632127'] = 382, + ['44250988'] = 252, + ['44947779'] = 182, + ['46455299'] = 253, + ['49764585'] = 384, + ['52804720'] = 90, + ['54988908'] = 184, + ['55441868'] = 162, + ['56746834'] = 162, + ['60571116'] = 52, + ['60601317'] = 303, + ['62800776'] = 142, + ['63107562'] = 168, + ['64014458'] = 297, + }, + ['56746834'] = { + ['31660270'] = 383, + ['40795725'] = 311, + ['41632127'] = 430, + ['44250988'] = 414, + ['44947779'] = 224, + ['46455299'] = 415, + ['49764585'] = 329, + ['52804720'] = 252, + ['54988908'] = 309, + ['55441868'] = 306, + ['55735743'] = 162, + ['60571116'] = 214, + ['60601317'] = 222, + ['62800776'] = 217, + ['63107562'] = 316, + ['64014458'] = 445, + }, + ['60541588'] = { + ['38447540'] = 761, + ['43159055'] = 260, + ['44267696'] = 782, + ['44825941'] = 189, + ['49442172'] = 663, + ['50543880'] = 267, + ['55498695'] = 285, + ['55533051'] = 197, + ['61540073'] = 547, + ['67153739'] = 636, + ['69752216'] = 774, + }, + ['60571116'] = { + ['31660270'] = 360, + ['40795725'] = 218, + ['40973830'] = 299, + ['41632127'] = 450, + ['44250988'] = 320, + ['44947779'] = 250, + ['49764585'] = 452, + ['52804720'] = 158, + ['54988908'] = 252, + ['55441868'] = 231, + ['55735743'] = 69, + ['56746834'] = 231, + ['60601317'] = 345, + ['62800776'] = 210, + ['63107562'] = 236, + ['64014458'] = 366, + }, + ['60601317'] = { + ['31660270'] = 400, + ['40795725'] = 429, + ['40973830'] = 510, + ['41632127'] = 233, + ['44250988'] = 200, + ['44947779'] = 304, + ['46455299'] = 532, + ['49764585'] = 108, + ['52804720'] = 223, + ['53793668'] = 653, + ['54988908'] = 87, + ['55441868'] = 439, + ['55735743'] = 301, + ['56746834'] = 222, + ['60571116'] = 331, + ['62800776'] = 350, + ['63107562'] = 448, + ['64014458'] = 577, + }, + ['61540073'] = { + ['38447540'] = 215, + ['43159055'] = 651, + ['44267696'] = 259, + ['44825941'] = 571, + ['49442172'] = 117, + ['50543880'] = 314, + ['55498695'] = 259, + ['55533051'] = 344, + ['60541588'] = 539, + ['67153739'] = 91, + ['69752216'] = 229, + }, + ['62800776'] = { + ['31660270'] = 385, + ['40795725'] = 260, + ['40973830'] = 250, + ['41632127'] = 492, + ['44250988'] = 361, + ['44947779'] = 224, + ['46455299'] = 252, + ['49764585'] = 494, + ['52804720'] = 200, + ['53793668'] = 361, + ['54988908'] = 294, + ['55441868'] = 89, + ['55735743'] = 110, + ['56746834'] = 229, + ['60571116'] = 161, + ['60601317'] = 417, + ['63107562'] = 99, + ['64014458'] = 319, + }, + ['63107562'] = { + ['31660270'] = 416, + ['40795725'] = 322, + ['40973830'] = 253, + ['41632127'] = 553, + ['44250988'] = 423, + ['44947779'] = 257, + ['46455299'] = 232, + ['49764585'] = 556, + ['52804720'] = 263, + ['53793668'] = 264, + ['54988908'] = 357, + ['55441868'] = 94, + ['55735743'] = 172, + ['56746834'] = 335, + ['60571116'] = 223, + ['62800776'] = 121, + ['64014458'] = 131, + }, + ['64014458'] = { + ['31660270'] = 550, + ['40795725'] = 456, + ['40973830'] = 388, + ['41632127'] = 688, + ['44250988'] = 558, + ['44947779'] = 392, + ['46455299'] = 195, + ['49764585'] = 691, + ['52804720'] = 397, + ['53793668'] = 134, + ['55441868'] = 228, + ['55735743'] = 307, + ['56746834'] = 470, + ['60571116'] = 357, + ['60601317'] = 584, + ['62800776'] = 304, + ['63107562'] = 135, + }, + ['67153739'] = { + ['38447540'] = 257, + ['43159055'] = 743, + ['44267696'] = 284, + ['44825941'] = 663, + ['49442172'] = 159, + ['50543880'] = 407, + ['55498695'] = 351, + ['55533051'] = 437, + ['60541588'] = 631, + ['61540073'] = 93, + ['69752216'] = 139, + }, + ['69752216'] = { + ['38447540'] = 294, + ['43159055'] = 884, + ['44267696'] = 262, + ['44825941'] = 804, + ['49442172'] = 301, + ['50543880'] = 547, + ['55498695'] = 492, + ['55533051'] = 578, + ['60541588'] = 773, + ['61540073'] = 234, + ['67153739'] = 141, + }, + }, +} + +-- Turtle WoW self-learned flight times (name-based, cross-account) +-- Synced from SavedVariables; shared across all accounts via addon file +-- Last sync: 2026-03-09 +NanamiLearnedFlights = { + -- 铁炉堡 / 铁炉堡机场 区域 + ["铁炉堡机场, 丹莫罗->米奈希尔港,湿地"] = 46, + ["米奈希尔港,湿地->铁炉堡机场, 丹莫罗"] = 43, + ["铁炉堡机场, 丹莫罗->丹阿格拉斯, 湿地"] = 25, + ["丹阿格拉斯, 湿地->铁炉堡机场, 丹莫罗"] = 29, + ["铁炉堡机场, 丹莫罗->铁炉堡,丹莫罗"] = 80, + ["铁炉堡,丹莫罗->铁炉堡机场, 丹莫罗"] = 80, + ["铁炉堡机场, 丹莫罗->南海镇,希尔斯布莱德"] = 138, + ["铁炉堡机场, 丹莫罗->暴风城,艾尔文森林"] = 237, + ["铁炉堡机场, 丹莫罗->哨兵岭,西部荒野"] = 297, + ["铁炉堡机场, 丹莫罗->丹基塔斯,冷酷海岸"] = 193, + ["铁炉堡机场, 丹莫罗->塞尔萨玛,洛克莫丹"] = 86, + ["铁炉堡机场, 丹莫罗->阿尔萨拉斯"] = 479, + ["铁炉堡,丹莫罗->丹阿格拉斯, 湿地"] = 107, + ["铁炉堡,丹莫罗->暴风城,艾尔文森林"] = 198, + ["铁炉堡,丹莫罗->丹基塔斯,冷酷海岸"] = 202, + -- 暴风城 区域 + ["暴风城,艾尔文森林->铁炉堡机场, 丹莫罗"] = 286, + ["暴风城,艾尔文森林->铁炉堡,丹莫罗"] = 244, + ["暴风城,艾尔文森林->藏宝海湾,荆棘谷"] = 230, + ["暴风城,艾尔文森林->摩根的岗哨,燃烧平原"] = 148, + ["暴风城,艾尔文森林->丹阿格拉斯, 湿地"] = 309, + ["暴风城,艾尔文森林->丹基塔斯,冷酷海岸"] = 406, + -- 丹阿格拉斯 区域 + ["丹阿格拉斯, 湿地->米奈希尔港,湿地"] = 20, + ["丹阿格拉斯, 湿地->铁炉堡,丹莫罗"] = 110, + ["丹阿格拉斯, 湿地->藏宝海湾,荆棘谷"] = 484, + ["丹阿格拉斯, 湿地->卡兰之墓,拉匹迪斯之岛"] = 605, + ["丹阿格拉斯, 湿地->塞尔萨玛,洛克莫丹"] = 150, + ["丹阿格拉斯, 湿地->暴风城,艾尔文森林"] = 266, + -- 米奈希尔港 区域 + ["米奈希尔港,湿地->丹阿格拉斯, 湿地"] = 28, + -- 南海镇 区域 + ["南海镇,希尔斯布莱德->丹阿格拉斯, 湿地"] = 129, + -- 摩根的岗哨 / 瑟银哨塔 区域 + ["摩根的岗哨,燃烧平原->瑟银哨塔,灼热峡谷"] = 97, + ["瑟银哨塔,灼热峡谷->摩根的岗哨,燃烧平原"] = 90, + ["瑟银哨塔,灼热峡谷->藏宝海湾,荆棘谷"] = 329, + -- 藏宝海湾 区域 + ["藏宝海湾,荆棘谷->铁炉堡机场, 丹莫罗"] = 460, + ["藏宝海湾,荆棘谷->卡兰之墓,拉匹迪斯之岛"] = 121, + ["藏宝海湾,荆棘谷->丹阿格拉斯, 湿地"] = 483, + ["卡兰之墓,拉匹迪斯之岛->藏宝海湾,荆棘谷"] = 121, + -- 加基森 / 塞拉摩 区域 + ["加基森,塔纳利斯->塞拉摩,尘泥沼泽"] = 144, + ["塞拉摩,尘泥沼泽->加基森,塔纳利斯"] = 147, + -- 安伯郡 / 哨兵岭 区域 + ["安伯郡,北风领->暴风城,艾尔文森林"] = 107, + ["哨兵岭,西部荒野->丹阿格拉斯, 湿地"] = 377, + ["哨兵岭,西部荒野->铁炉堡机场, 丹莫罗"] = 354, + -- 丹基塔斯 (Turtle WoW 冷酷海岸) + ["丹基塔斯,冷酷海岸->铁炉堡,丹莫罗"] = 210, + ["丹基塔斯,冷酷海岸->铁炉堡机场, 丹莫罗"] = 204, + ["丹基塔斯,冷酷海岸->暴风城,艾尔文森林"] = 371, + ["丹基塔斯,冷酷海岸->哨兵岭,西部荒野"] = 431, + ["丹基塔斯,冷酷海岸->塞尔萨玛,洛克莫丹"] = 107, + -- 塞尔萨玛 区域 + ["塞尔萨玛,洛克莫丹->铁炉堡机场, 丹莫罗"] = 96, + -- 阿尔萨拉斯 / 圣光之愿 / 鹰巢山 区域 + ["阿尔萨拉斯->鹰巢山,辛特兰"] = 276, + ["阿尔萨拉斯->圣光之愿礼拜堂,东瘟疫之地"] = 125, + ["圣光之愿礼拜堂,东瘟疫之地->阿尔萨拉斯"] = 124, + ["鹰巢山,辛特兰->丹阿格拉斯, 湿地"] = 190, + -- 月光林地 / 石爪峰 区域 + ["月光林地->石爪峰,石爪山"] = 304, + ["石爪峰,石爪山->塔伦迪斯营地,艾萨拉"] = 285, +} \ No newline at end of file diff --git a/FlightMap.lua b/FlightMap.lua new file mode 100644 index 0000000..efb9b28 --- /dev/null +++ b/FlightMap.lua @@ -0,0 +1,1282 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Flight Map (FlightMap.lua) +-- Skins TaxiFrame with Nanami-UI theme, destination list, in-flight timer +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.FlightMap = {} +local FM = SFrames.FlightMap +SFramesDB = SFramesDB or {} +SFramesGlobalDB = SFramesGlobalDB or {} + +-------------------------------------------------------------------------------- +-- Theme (Pink Cat-Paw) +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + currentText = { 0.40, 1.0, 0.40 }, + moneyGold = { 1, 0.84, 0.0 }, + moneySilver = { 0.78, 0.78, 0.78 }, + moneyCopper = { 0.71, 0.43, 0.18 }, + arrivedText = { 0.40, 1.0, 0.40 }, +}) + +-------------------------------------------------------------------------------- +-- Layout +-------------------------------------------------------------------------------- +local DEST_PANEL_W = 220 +local DEST_ROW_H = 22 +local HEADER_H = 34 +local SIDE_PAD = 10 +local MAX_DEST_ROWS = 30 + +local BAR_W = 160 +local TRACK_H = 170 +local TRACK_X = 20 + +-------------------------------------------------------------------------------- +-- State +-------------------------------------------------------------------------------- +local skinApplied = false +local DestPanel = nil +local DestRows = {} +local FlightBar = nil + +local flightState = { + pendingFlight = false, + inFlight = false, + source = "", + dest = "", + startTime = 0, + estimated = 0, + lingerTime = 0, +} + +FM.currentSource = "" + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function FormatMoney(copper) + if not copper or copper <= 0 then return 0, 0, 0 end + local g = math.floor(copper / 10000) + local s = math.floor(math.mod(copper, 10000) / 100) + local c = math.mod(copper, 100) + return g, s, c +end + +local function FormatTime(seconds) + if not seconds or seconds < 0 then seconds = 0 end + local m = math.floor(seconds / 60) + local s = math.floor(math.mod(seconds, 60)) + return string.format("%d:%02d", m, s) +end + +local function StripTextures(frame) + if not frame or not frame.GetRegions then return end + local regions = { frame:GetRegions() } + for i = 1, table.getn(regions) do + local region = regions[i] + if region and region.SetTexture and not region._nanamiKeep then + local drawLayer = region.GetDrawLayer and region:GetDrawLayer() + if drawLayer == "BACKGROUND" or drawLayer == "BORDER" or drawLayer == "ARTWORK" then + region:SetTexture(nil) + region:SetAlpha(0) + region:Hide() + end + end + end +end + +local function CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.45) + s:SetBackdropBorderColor(0, 0, 0, 0.6) + s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) + return s +end + +-------------------------------------------------------------------------------- +-- Flight Time Database (self-learning, stored in SFramesGlobalDB) +-------------------------------------------------------------------------------- +local function GetFlightDB() + if not SFramesGlobalDB then SFramesGlobalDB = {} end + if not SFramesGlobalDB.flightTimes then SFramesGlobalDB.flightTimes = {} end + return SFramesGlobalDB.flightTimes +end + +local function GetRouteKey(src, dst) + return (src or "") .. "->" .. (dst or "") +end + +local function GetTaxiNodeHash(nodeIndex) + local x, y = TaxiNodePosition(nodeIndex) + if not x then return nil end + return tostring(math.floor(x * 100000000)) +end + +local function GetPlayerFaction() + local faction = UnitFactionGroup("player") + if faction == "Alliance" then return "Alliance" end + return "Horde" +end + +-- Hash-based index built when taxi map opens: hash -> nodeIndex +local hashToIndex = {} +local indexToHash = {} +local hashCorrection = {} -- actualHash -> ftcHash (fuzzy match) + +local function BuildHashIndex() + hashToIndex = {} + indexToHash = {} + hashCorrection = {} + local numNodes = NumTaxiNodes() + local faction = GetPlayerFaction() + local factionData = FTCData and FTCData[faction] + + -- Collect all hashes used in FTCData for fuzzy matching + local ftcHashes = {} + if factionData then + for srcH, routes in pairs(factionData) do + ftcHashes[srcH] = true + for dstH in pairs(routes) do + ftcHashes[dstH] = true + end + end + end + + for i = 1, numNodes do + local h = GetTaxiNodeHash(i) + if h then + hashToIndex[h] = i + indexToHash[i] = h + if ftcHashes[h] then + hashCorrection[h] = h + else + -- Turtle WoW coords may differ slightly from Classic; try ±10 + local hNum = tonumber(h) + if hNum then + for delta = -10, 10 do + local candidate = tostring(hNum + delta) + if ftcHashes[candidate] then + hashCorrection[h] = candidate + break + end + end + end + end + end + end +end + +local function LookupFTCData(srcHash, dstHash) + if not srcHash or not dstHash then return nil end + local faction = GetPlayerFaction() + -- Priority 1: self-learned hash DB (exact hash, no correction needed) + local hdb = SFramesGlobalDB and SFramesGlobalDB.flightTimesHash + if hdb and hdb[faction] then + local lr = hdb[faction][srcHash] + if lr and lr[dstHash] then return lr[dstHash] end + end + -- Priority 2: FTCData pre-recorded (with fuzzy hash correction) + if not FTCData then return nil end + local factionData = FTCData[faction] + if not factionData then return nil end + local corrSrc = hashCorrection[srcHash] or srcHash + local corrDst = hashCorrection[dstHash] or dstHash + local srcRoutes = factionData[corrSrc] + if not srcRoutes then return nil end + return srcRoutes[corrDst] +end + +local function GetEstimatedTime(src, dst) + local routeKey = GetRouteKey(src, dst) + -- Priority 1: per-account learned database (SavedVariables) + local db = GetFlightDB() + local learned = db[routeKey] + if learned then return learned end + + -- Priority 2: shared learned database (FlightData.lua, cross-account) + if NanamiLearnedFlights and NanamiLearnedFlights[routeKey] then + return NanamiLearnedFlights[routeKey] + end + + -- Priority 3: FTCData pre-recorded database (by hash) + if FM.currentSourceHash and dst then + local numNodes = NumTaxiNodes() + for i = 1, numNodes do + if TaxiNodeName(i) == dst then + local dstHash = indexToHash[i] or GetTaxiNodeHash(i) + if dstHash then + local ftcTime = LookupFTCData(FM.currentSourceHash, dstHash) + if ftcTime then return ftcTime end + end + break + end + end + end + + return nil +end + +local function GetEstimatedTimeByHash(srcHash, dstHash) + -- Check learned DB by resolving hash to names + local srcIdx = hashToIndex[srcHash] + local dstIdx = hashToIndex[dstHash] + if srcIdx and dstIdx then + local db = GetFlightDB() + local learned = db[GetRouteKey(TaxiNodeName(srcIdx), TaxiNodeName(dstIdx))] + if learned then return learned end + end + return LookupFTCData(srcHash, dstHash) +end + +local function GetFlightHashDB() + if not SFramesGlobalDB then SFramesGlobalDB = {} end + if not SFramesGlobalDB.flightTimesHash then SFramesGlobalDB.flightTimesHash = {} end + return SFramesGlobalDB.flightTimesHash +end + +local function SaveFlightTime(src, dst, duration, srcHash, dstHash) + if not src or src == "" or not dst or dst == "" then return end + if duration < 5 then return end + local secs = math.floor(duration + 0.5) + local db = GetFlightDB() + db[GetRouteKey(src, dst)] = secs + -- Also save in hash format for cross-account export + if srcHash and dstHash then + local faction = GetPlayerFaction() + local hdb = GetFlightHashDB() + if not hdb[faction] then hdb[faction] = {} end + if not hdb[faction][srcHash] then hdb[faction][srcHash] = {} end + hdb[faction][srcHash][dstHash] = secs + end +end + +-------------------------------------------------------------------------------- +-- Money Layout (icon-based, like Merchant.lua) +-------------------------------------------------------------------------------- +local function LayoutRowMoney(row, copper) + row.gTxt:Hide(); row.gTex:Hide() + row.sTxt:Hide(); row.sTex:Hide() + row.cTxt:Hide(); row.cTex:Hide() + + if not copper or copper <= 0 then + row.gTxt:SetText("--") + row.gTxt:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + row.gTxt:ClearAllPoints() + row.gTxt:SetPoint("RIGHT", row, "RIGHT", -6, 0) + row.gTxt:Show() + return + end + + local vG, vS, vC = FormatMoney(copper) + local anchor = nil + + local function AttachPair(txt, tex, val, cr, cg, cb) + txt:SetText(val) + txt:SetTextColor(cr, cg, cb) + txt:ClearAllPoints() + if not anchor then + txt:SetPoint("RIGHT", row, "RIGHT", -6, 0) + else + txt:SetPoint("RIGHT", anchor, "LEFT", -3, 0) + end + txt:Show() + tex:ClearAllPoints() + tex:SetPoint("RIGHT", txt, "LEFT", -1, 0) + tex:Show() + anchor = tex + end + + if vC > 0 then AttachPair(row.cTxt, row.cTex, vC, T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3]) end + if vS > 0 then AttachPair(row.sTxt, row.sTex, vS, T.moneySilver[1], T.moneySilver[2], T.moneySilver[3]) end + if vG > 0 then AttachPair(row.gTxt, row.gTex, vG, T.moneyGold[1], T.moneyGold[2], T.moneyGold[3]) end +end + +-------------------------------------------------------------------------------- +-- Destination Row Factory +-------------------------------------------------------------------------------- +local function CreateDestRow(parent, index) + local row = CreateFrame("Button", "NanamiFlightDest" .. index, parent) + row:SetHeight(DEST_ROW_H) + + local font = GetFont() + + local dot = row:CreateTexture(nil, "ARTWORK") + dot:SetTexture("Interface\\Buttons\\WHITE8X8") + dot:SetWidth(6) + dot:SetHeight(6) + dot:SetPoint("LEFT", row, "LEFT", 6, 0) + row.dot = dot + + local nameFS = row:CreateFontString(nil, "OVERLAY") + nameFS:SetFont(font, 10, "OUTLINE") + nameFS:SetPoint("LEFT", dot, "RIGHT", 5, 0) + nameFS:SetPoint("RIGHT", row, "RIGHT", -112, 0) + nameFS:SetJustifyH("LEFT") + row.nameFS = nameFS + + local timeFS = row:CreateFontString(nil, "OVERLAY") + timeFS:SetFont(font, 9, "OUTLINE") + timeFS:SetPoint("RIGHT", row, "RIGHT", -78, 0) + timeFS:SetJustifyH("RIGHT") + timeFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + row.timeFS = timeFS + + row.gTxt = row:CreateFontString(nil, "OVERLAY") + row.gTxt:SetFont(font, 9, "OUTLINE") + row.gTex = row:CreateTexture(nil, "ARTWORK") + row.gTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") + row.gTex:SetTexCoord(0, 0.25, 0, 1) + row.gTex:SetWidth(10); row.gTex:SetHeight(10) + + row.sTxt = row:CreateFontString(nil, "OVERLAY") + row.sTxt:SetFont(font, 9, "OUTLINE") + row.sTex = row:CreateTexture(nil, "ARTWORK") + row.sTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") + row.sTex:SetTexCoord(0.25, 0.5, 0, 1) + row.sTex:SetWidth(10); row.sTex:SetHeight(10) + + row.cTxt = row:CreateFontString(nil, "OVERLAY") + row.cTxt:SetFont(font, 9, "OUTLINE") + row.cTex = row:CreateTexture(nil, "ARTWORK") + row.cTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") + row.cTex:SetTexCoord(0.5, 0.75, 0, 1) + row.cTex:SetWidth(10); row.cTex:SetHeight(10) + + local hl = row:CreateTexture(nil, "HIGHLIGHT") + hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + hl:SetBlendMode("ADD") + hl:SetAllPoints(row) + hl:SetAlpha(0.15) + + row.nodeIndex = nil + row.nodeType = nil + + row:SetScript("OnEnter", function() + if this.nodeType == "REACHABLE" and this.nodeIndex then + GameTooltip:SetOwner(this, "ANCHOR_LEFT") + GameTooltip:AddLine(TaxiNodeName(this.nodeIndex), 1, 1, 1) + local cost = TaxiNodeCost(this.nodeIndex) + if cost and cost > 0 then + SetTooltipMoney(GameTooltip, cost) + end + local est = GetEstimatedTime(FM.currentSource, TaxiNodeName(this.nodeIndex)) + if est then + GameTooltip:AddLine(" ") + GameTooltip:AddLine("预计飞行: " .. FormatTime(est), 0.6, 0.8, 1.0) + end + GameTooltip:AddLine(" ") + GameTooltip:AddLine("点击飞往此处", T.dimText[1], T.dimText[2], T.dimText[3]) + GameTooltip:Show() + end + end) + + row:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + row:SetScript("OnClick", function() + if this.nodeType == "REACHABLE" and this.nodeIndex then + TakeTaxiNode(this.nodeIndex) + end + end) + + return row +end + +-------------------------------------------------------------------------------- +-- Skin TaxiFrame +-------------------------------------------------------------------------------- +function FM:ApplySkin() + if skinApplied then return end + if not TaxiFrame then return end + skinApplied = true + + local font = GetFont() + + StripTextures(TaxiFrame) + + if TaxiPortrait then TaxiPortrait:Hide(); TaxiPortrait:SetAlpha(0) end + if TaxiTitleText then TaxiTitleText:Hide() end + + local origClose = TaxiCloseButton + if origClose then + origClose:Hide() + origClose:SetAlpha(0) + origClose.Show = function() end + end + + TaxiFrame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + TaxiFrame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + TaxiFrame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) + + CreateShadow(TaxiFrame) + + -- Trim empty space at the bottom (TaxiMap anchored to top, safe to shrink from bottom) + if TaxiMap then + local mapBottom = TaxiMap:GetBottom() + local frameBottom = TaxiFrame:GetBottom() + if mapBottom and frameBottom and mapBottom > frameBottom then + local excess = mapBottom - frameBottom - 10 + if excess > 10 then + TaxiFrame:SetHeight(TaxiFrame:GetHeight() - excess) + end + end + end + + TaxiFrame:SetMovable(true) + TaxiFrame:EnableMouse(true) + TaxiFrame:RegisterForDrag("LeftButton") + TaxiFrame:SetScript("OnDragStart", function() this:StartMoving() end) + TaxiFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + local header = CreateFrame("Frame", nil, TaxiFrame) + header:SetPoint("TOPLEFT", TaxiFrame, "TOPLEFT", 0, 0) + header:SetPoint("TOPRIGHT", TaxiFrame, "TOPRIGHT", 0, 0) + header:SetHeight(HEADER_H) + header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) + header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) + header:SetFrameLevel(TaxiFrame:GetFrameLevel() + 5) + + local titleIco = SFrames:CreateIcon(header, "mount", 16) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0) + titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) + + local titleFS = header:CreateFontString(nil, "OVERLAY") + titleFS:SetFont(font, 14, "OUTLINE") + titleFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) + titleFS:SetPoint("RIGHT", header, "RIGHT", -30, 0) + titleFS:SetJustifyH("LEFT") + titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + FM.titleFS = titleFS + + local closeBtn = CreateFrame("Button", nil, header) + closeBtn:SetWidth(20) + closeBtn:SetHeight(20) + closeBtn:SetPoint("RIGHT", header, "RIGHT", -8, 0) + closeBtn:SetFrameLevel(header:GetFrameLevel() + 1) + local closeTex = closeBtn:CreateTexture(nil, "ARTWORK") + closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") + closeTex:SetTexCoord(0.25, 0.375, 0, 0.125) + closeTex:SetAllPoints() + closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) + closeBtn:SetScript("OnClick", function() CloseTaxiMap() end) + closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end) + closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) + + local headerSep = TaxiFrame:CreateTexture(nil, "OVERLAY") + headerSep:SetTexture("Interface\\Buttons\\WHITE8X8") + headerSep:SetHeight(1) + headerSep:SetPoint("TOPLEFT", TaxiFrame, "TOPLEFT", 4, -HEADER_H) + headerSep:SetPoint("TOPRIGHT", TaxiFrame, "TOPRIGHT", -4, -HEADER_H) + headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + headerSep._nanamiKeep = true + + if TaxiMap then + local mapBorder = CreateFrame("Frame", nil, TaxiFrame) + mapBorder:SetPoint("TOPLEFT", TaxiMap, "TOPLEFT", -3, 3) + mapBorder:SetPoint("BOTTOMRIGHT", TaxiMap, "BOTTOMRIGHT", 3, -3) + mapBorder:SetBackdrop({ + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + mapBorder:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) + mapBorder:SetFrameLevel(TaxiFrame:GetFrameLevel() + 3) + end + + FM:CreateDestPanel() + + local origOnHide = TaxiFrame:GetScript("OnHide") + TaxiFrame:SetScript("OnHide", function() + if origOnHide then origOnHide() end + if DestPanel then DestPanel:Hide() end + end) +end + +-------------------------------------------------------------------------------- +-- Destination Panel +-------------------------------------------------------------------------------- +function FM:CreateDestPanel() + local font = GetFont() + + DestPanel = CreateFrame("Frame", "NanamiFlightDestPanel", UIParent) + DestPanel:SetWidth(DEST_PANEL_W) + DestPanel:SetPoint("TOPLEFT", TaxiFrame, "TOPRIGHT", 4, 0) + DestPanel:SetPoint("BOTTOMLEFT", TaxiFrame, "BOTTOMRIGHT", 4, 0) + DestPanel:SetFrameStrata(TaxiFrame:GetFrameStrata()) + DestPanel:SetFrameLevel(TaxiFrame:GetFrameLevel() + 1) + DestPanel:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + DestPanel:SetBackdropColor(T.listBg[1], T.listBg[2], T.listBg[3], T.listBg[4]) + DestPanel:SetBackdropBorderColor(T.listBorder[1], T.listBorder[2], T.listBorder[3], T.listBorder[4]) + + CreateShadow(DestPanel) + + local panelTitle = DestPanel:CreateFontString(nil, "OVERLAY") + panelTitle:SetFont(font, 12, "OUTLINE") + panelTitle:SetPoint("TOP", DestPanel, "TOP", 0, -8) + panelTitle:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + panelTitle:SetText("目的地列表") + + FM.currentLabel = DestPanel:CreateFontString(nil, "OVERLAY") + FM.currentLabel:SetFont(font, 10, "OUTLINE") + FM.currentLabel:SetPoint("TOPLEFT", DestPanel, "TOPLEFT", 8, -28) + FM.currentLabel:SetPoint("RIGHT", DestPanel, "RIGHT", -8, 0) + FM.currentLabel:SetJustifyH("LEFT") + FM.currentLabel:SetTextColor(T.currentText[1], T.currentText[2], T.currentText[3]) + + local sepLine = DestPanel:CreateTexture(nil, "OVERLAY") + sepLine:SetTexture("Interface\\Buttons\\WHITE8X8") + sepLine:SetHeight(1) + sepLine:SetPoint("TOPLEFT", DestPanel, "TOPLEFT", 8, -46) + sepLine:SetPoint("TOPRIGHT", DestPanel, "TOPRIGHT", -8, -46) + sepLine:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + FM.noDestLabel = DestPanel:CreateFontString(nil, "OVERLAY") + FM.noDestLabel:SetFont(font, 10, "OUTLINE") + FM.noDestLabel:SetPoint("TOP", DestPanel, "TOP", 0, -60) + FM.noDestLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + FM.noDestLabel:SetText("暂无可用航线") + FM.noDestLabel:Hide() + + local scrollFrame = CreateFrame("ScrollFrame", "NanamiFlightDestScroll", DestPanel, "UIPanelScrollFrameTemplate") + scrollFrame:SetPoint("TOPLEFT", DestPanel, "TOPLEFT", 2, -50) + scrollFrame:SetPoint("BOTTOMRIGHT", DestPanel, "BOTTOMRIGHT", -20, 6) + + local scrollChild = CreateFrame("Frame", "NanamiFlightDestScrollChild", scrollFrame) + scrollChild:SetWidth(DEST_PANEL_W - 24) + scrollChild:SetHeight(MAX_DEST_ROWS * DEST_ROW_H + 20) + scrollFrame:SetScrollChild(scrollChild) + + local scrollBar = getglobal("NanamiFlightDestScrollScrollBar") + if scrollBar then + scrollBar:SetWidth(12) + local regions = { scrollBar:GetRegions() } + for i = 1, table.getn(regions) do + local region = regions[i] + if region and region.GetObjectType and region:GetObjectType() == "Texture" then + region:SetTexture(nil) + region:SetAlpha(0) + end + end + local thumb = scrollBar:GetThumbTexture() + if thumb then + thumb:SetTexture("Interface\\Buttons\\WHITE8X8") + thumb:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.6) + thumb:SetWidth(10) + thumb:SetHeight(40) + end + end + local scrollUp = getglobal("NanamiFlightDestScrollScrollBarScrollUpButton") + if scrollUp then scrollUp:SetAlpha(0); scrollUp:SetWidth(1); scrollUp:SetHeight(1) end + local scrollDown = getglobal("NanamiFlightDestScrollScrollBarScrollDownButton") + if scrollDown then scrollDown:SetAlpha(0); scrollDown:SetWidth(1); scrollDown:SetHeight(1) end + + for i = 1, MAX_DEST_ROWS do + local row = CreateDestRow(scrollChild, i) + row:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -((i - 1) * DEST_ROW_H)) + row:SetPoint("RIGHT", scrollChild, "RIGHT", 0, 0) + row:Hide() + DestRows[i] = row + end + + DestPanel:Hide() +end + +-------------------------------------------------------------------------------- +-- Update Destinations +-------------------------------------------------------------------------------- +function FM:UpdateDestinations() + if not DestPanel then return end + + local numNodes = NumTaxiNodes() + local currentName = "" + local reachable = {} + local unreachableCount = 0 + + for i = 1, numNodes do + local name = TaxiNodeName(i) + local ntype = TaxiNodeGetType(i) + local cost = TaxiNodeCost(i) + + if ntype == "CURRENT" then + currentName = name + elseif ntype == "REACHABLE" then + table.insert(reachable, { index = i, name = name, cost = cost }) + elseif ntype == "NONE" then + unreachableCount = unreachableCount + 1 + end + end + + FM.currentSource = currentName + + -- Build hash index for FTCData lookup + BuildHashIndex() + FM.currentSourceHash = nil + for i = 1, numNodes do + if TaxiNodeGetType(i) == "CURRENT" then + FM.currentSourceHash = indexToHash[i] or GetTaxiNodeHash(i) + break + end + end + + table.sort(reachable, function(a, b) return a.cost < b.cost end) + + local npcName = UnitName("NPC") or "飞行管理员" + FM.titleFS:SetText(npcName .. " - 飞行路线") + + if currentName ~= "" then + FM.currentLabel:SetText("|cFF66E666*|r " .. currentName) + else + FM.currentLabel:SetText("|cFF66E666*|r 未知") + end + + local rowIdx = 0 + + for _, dest in ipairs(reachable) do + rowIdx = rowIdx + 1 + if rowIdx <= MAX_DEST_ROWS then + local row = DestRows[rowIdx] + row.nodeIndex = dest.index + row.nodeType = "REACHABLE" + row.dot:SetVertexColor(T.nameText[1], T.nameText[2], T.nameText[3]) + row.nameFS:SetText(dest.name) + row.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + LayoutRowMoney(row, dest.cost) + local est = GetEstimatedTime(currentName, dest.name) + if not est and FM.currentSourceHash then + local dstHash = indexToHash[dest.index] or GetTaxiNodeHash(dest.index) + if dstHash then est = LookupFTCData(FM.currentSourceHash, dstHash) end + end + if est then + row.timeFS:SetText(FormatTime(est)) + row.timeFS:Show() + else + row.timeFS:SetText("") + row.timeFS:Hide() + end + row:Show() + end + end + + if unreachableCount > 0 then + rowIdx = rowIdx + 1 + if rowIdx <= MAX_DEST_ROWS then + local row = DestRows[rowIdx] + row.nodeIndex = nil + row.nodeType = "NONE" + row.dot:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) + row.nameFS:SetText("(" .. unreachableCount .. " 个未发现)") + row.nameFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + row.gTxt:Hide(); row.gTex:Hide() + row.sTxt:Hide(); row.sTex:Hide() + row.cTxt:Hide(); row.cTex:Hide() + row.timeFS:SetText(""); row.timeFS:Hide() + row:Show() + end + end + + for i = rowIdx + 1, MAX_DEST_ROWS do + DestRows[i]:Hide() + end + + if rowIdx == 0 then + FM.noDestLabel:Show() + else + FM.noDestLabel:Hide() + end + + local child = getglobal("NanamiFlightDestScrollChild") + if child then + child:SetHeight(math.max(rowIdx * DEST_ROW_H + 10, 50)) + end + + DestPanel:Show() +end + +-------------------------------------------------------------------------------- +-- Hook Node Buttons (tooltip only) +-------------------------------------------------------------------------------- +function FM:HookNodeButtons() + local numNodes = NumTaxiNodes() + for i = 1, numNodes do + local btn = getglobal("TaxiButton" .. i) + if btn and not btn._nanamiHooked then + btn._nanamiHooked = true + + local origEnter = btn:GetScript("OnEnter") + btn:SetScript("OnEnter", function() + if origEnter then origEnter() end + local id = this:GetID() + if TaxiNodeGetType(id) == "REACHABLE" then + local cost = TaxiNodeCost(id) + if cost and cost > 0 then + GameTooltip:AddLine(" ") + SetTooltipMoney(GameTooltip, cost) + end + local est = GetEstimatedTime(FM.currentSource, TaxiNodeName(id)) + if est then + GameTooltip:AddLine("预计飞行: " .. FormatTime(est), 0.6, 0.8, 1.0) + end + GameTooltip:Show() + end + for _, row in ipairs(DestRows) do + if row:IsShown() and row.nodeIndex == id then + row.nameFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + end + end + end) + + local origLeave = btn:GetScript("OnLeave") + btn:SetScript("OnLeave", function() + if origLeave then origLeave() end + for _, row in ipairs(DestRows) do + if row:IsShown() and row.nodeType == "REACHABLE" then + row.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + end + end + end) + end + end +end + +-------------------------------------------------------------------------------- +-- Flight Progress Bar (vertical axis: departure → destination with countdown) +-------------------------------------------------------------------------------- +local COLLAPSED_H = 34 +local barCollapsed = false + +local function SetBarCollapsed(collapsed) + barCollapsed = collapsed + if not FlightBar then return end + local bar = FlightBar + if collapsed then + bar:SetHeight(COLLAPSED_H) + for _, el in ipairs(bar.expandElements) do el:Hide() end + bar.collapseBtn.label:SetText("+") + -- Vertically center all visible elements + bar.titleFS:ClearAllPoints() + bar.titleFS:SetPoint("LEFT", bar, "LEFT", 10, 0) + bar.compactFS:ClearAllPoints() + bar.compactFS:SetPoint("RIGHT", bar, "RIGHT", -28, 0) + bar.collapseBtn:ClearAllPoints() + bar.collapseBtn:SetPoint("RIGHT", bar, "RIGHT", -6, 0) + bar.compactFS:Show() + else + bar:SetHeight(bar.expandedH) + for _, el in ipairs(bar.expandElements) do el:Show() end + bar.collapseBtn.label:SetText("-") + -- Restore top-aligned positions + bar.titleFS:ClearAllPoints() + bar.titleFS:SetPoint("TOPLEFT", bar, "TOPLEFT", 10, -8) + bar.compactFS:ClearAllPoints() + bar.compactFS:SetPoint("RIGHT", bar, "RIGHT", -28, -9) + bar.collapseBtn:ClearAllPoints() + bar.collapseBtn:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -6, -6) + bar.compactFS:Hide() + end +end + +local function CreateFlightBar() + if FlightBar then return end + + local font = GetFont() + local panelH = 8 + 18 + 6 + 12 + 6 + TRACK_H + 6 + 12 + 10 + 14 + 4 + 14 + 4 + 14 + 10 + + local bar = CreateFrame("Frame", "NanamiFlightBar", UIParent) + bar:SetWidth(BAR_W) + bar:SetHeight(panelH) + bar.expandedH = panelH + bar:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", -80, -300) + bar:SetFrameStrata("HIGH") + bar:SetMovable(true) + bar:EnableMouse(true) + bar:RegisterForDrag("LeftButton") + bar:SetScript("OnDragStart", function() this:StartMoving() end) + bar:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + bar:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + bar:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + bar:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) + CreateShadow(bar) + + bar.expandElements = {} + local yOff = -8 + + -- Title (always visible) + bar.titleFS = bar:CreateFontString(nil, "OVERLAY") + bar.titleFS:SetFont(font, 13, "OUTLINE") + bar.titleFS:SetPoint("TOPLEFT", bar, "TOPLEFT", 10, yOff) + bar.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + bar.titleFS:SetText("飞行中...") + + -- Compact remaining time (shown only when collapsed, next to title) + bar.compactFS = bar:CreateFontString(nil, "OVERLAY") + bar.compactFS:SetFont(font, 13, "OUTLINE") + bar.compactFS:SetPoint("RIGHT", bar, "RIGHT", -28, yOff - 1) + bar.compactFS:SetJustifyH("RIGHT") + bar.compactFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + bar.compactFS:Hide() + + -- Collapse / expand button + local colBtn = CreateFrame("Button", nil, bar) + colBtn:SetWidth(18) + colBtn:SetHeight(18) + colBtn:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -6, -6) + colBtn:SetFrameLevel(bar:GetFrameLevel() + 3) + local colLabel = colBtn:CreateFontString(nil, "OVERLAY") + colLabel:SetFont(font, 14, "OUTLINE") + colLabel:SetPoint("CENTER", 0, 1) + colLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + colLabel:SetText("-") + colBtn.label = colLabel + colBtn:SetScript("OnClick", function() + SetBarCollapsed(not barCollapsed) + end) + colBtn:SetScript("OnEnter", function() + colLabel:SetTextColor(1, 0.7, 0.85) + end) + colBtn:SetScript("OnLeave", function() + colLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + end) + bar.collapseBtn = colBtn + + yOff = yOff - 18 + + -- Separator + local sep1 = bar:CreateTexture(nil, "OVERLAY") + sep1:SetTexture("Interface\\Buttons\\WHITE8X8") + sep1:SetHeight(1) + sep1:SetPoint("TOPLEFT", bar, "TOPLEFT", 8, yOff - 2) + sep1:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -8, yOff - 2) + sep1:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + table.insert(bar.expandElements, sep1) + yOff = yOff - 6 + + -- Source dot + name + local srcDot = bar:CreateTexture(nil, "OVERLAY") + srcDot:SetTexture("Interface\\Buttons\\WHITE8X8") + srcDot:SetWidth(9) + srcDot:SetHeight(9) + srcDot:SetPoint("TOPLEFT", bar, "TOPLEFT", TRACK_X, yOff) + srcDot:SetVertexColor(T.currentText[1], T.currentText[2], T.currentText[3]) + bar.srcDot = srcDot + table.insert(bar.expandElements, srcDot) + + bar.srcNameFS = bar:CreateFontString(nil, "OVERLAY") + bar.srcNameFS:SetFont(font, 10, "OUTLINE") + bar.srcNameFS:SetPoint("LEFT", srcDot, "RIGHT", 6, 0) + bar.srcNameFS:SetPoint("RIGHT", bar, "RIGHT", -8, 0) + bar.srcNameFS:SetJustifyH("LEFT") + bar.srcNameFS:SetTextColor(T.currentText[1], T.currentText[2], T.currentText[3]) + table.insert(bar.expandElements, bar.srcNameFS) + + yOff = yOff - 14 + + -- Vertical track area + local trackTop = yOff + local trackBg = bar:CreateTexture(nil, "BACKGROUND") + trackBg:SetTexture("Interface\\Buttons\\WHITE8X8") + trackBg:SetWidth(3) + trackBg:SetHeight(TRACK_H) + trackBg:SetPoint("TOPLEFT", bar, "TOPLEFT", TRACK_X + 3, trackTop) + trackBg:SetVertexColor(T.sectionBg[1], T.sectionBg[2], T.sectionBg[3], T.sectionBg[4]) + bar.trackBg = trackBg + table.insert(bar.expandElements, trackBg) + + -- Track fill (grows from top downward) + local trackFillFrame = CreateFrame("Frame", nil, bar) + trackFillFrame:SetPoint("TOPLEFT", trackBg, "TOPLEFT", 0, 0) + trackFillFrame:SetWidth(3) + trackFillFrame:SetHeight(1) + local trackFill = trackFillFrame:CreateTexture(nil, "ARTWORK") + trackFill:SetTexture("Interface\\Buttons\\WHITE8X8") + trackFill:SetAllPoints(trackFillFrame) + trackFill:SetVertexColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], T.progressFill[4]) + bar.trackFillFrame = trackFillFrame + bar.trackFill = trackFill + table.insert(bar.expandElements, trackFillFrame) + + -- Progress indicator + local progDot = bar:CreateTexture(nil, "OVERLAY") + progDot:SetTexture("Interface\\Buttons\\WHITE8X8") + progDot:SetWidth(13) + progDot:SetHeight(5) + progDot:SetPoint("CENTER", trackBg, "TOP", 0, 0) + progDot:SetVertexColor(T.accent[1], T.accent[2], T.accent[3], T.accent[4]) + bar.progDot = progDot + table.insert(bar.expandElements, progDot) + + -- Progress glow + local progGlow = bar:CreateTexture(nil, "ARTWORK") + progGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + progGlow:SetBlendMode("ADD") + progGlow:SetWidth(28) + progGlow:SetHeight(28) + progGlow:SetPoint("CENTER", progDot, "CENTER", 0, 0) + progGlow:SetVertexColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 0.4) + bar.progGlow = progGlow + table.insert(bar.expandElements, progGlow) + + -- Elapsed time + bar.elapsedFS = bar:CreateFontString(nil, "OVERLAY") + bar.elapsedFS:SetFont(font, 11, "OUTLINE") + bar.elapsedFS:SetPoint("LEFT", trackBg, "RIGHT", 12, 0) + bar.elapsedFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + table.insert(bar.expandElements, bar.elapsedFS) + + -- Remaining time (follows progress dot) + bar.remainFS = bar:CreateFontString(nil, "OVERLAY") + bar.remainFS:SetFont(font, 14, "OUTLINE") + bar.remainFS:SetPoint("LEFT", progDot, "RIGHT", 10, 0) + bar.remainFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + table.insert(bar.expandElements, bar.remainFS) + + yOff = trackTop - TRACK_H + + -- Destination dot + name + local dstDot = bar:CreateTexture(nil, "OVERLAY") + dstDot:SetTexture("Interface\\Buttons\\WHITE8X8") + dstDot:SetWidth(9) + dstDot:SetHeight(9) + dstDot:SetPoint("TOPLEFT", bar, "TOPLEFT", TRACK_X, yOff - 4) + dstDot:SetVertexColor(T.nameText[1], T.nameText[2], T.nameText[3]) + bar.dstDot = dstDot + table.insert(bar.expandElements, dstDot) + + bar.dstNameFS = bar:CreateFontString(nil, "OVERLAY") + bar.dstNameFS:SetFont(font, 10, "OUTLINE") + bar.dstNameFS:SetPoint("LEFT", dstDot, "RIGHT", 6, 0) + bar.dstNameFS:SetPoint("RIGHT", bar, "RIGHT", -8, 0) + bar.dstNameFS:SetJustifyH("LEFT") + bar.dstNameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + table.insert(bar.expandElements, bar.dstNameFS) + + yOff = yOff - 18 + + -- Bottom info + local sep2 = bar:CreateTexture(nil, "OVERLAY") + sep2:SetTexture("Interface\\Buttons\\WHITE8X8") + sep2:SetHeight(1) + sep2:SetPoint("TOPLEFT", bar, "TOPLEFT", 8, yOff) + sep2:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -8, yOff) + sep2:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + table.insert(bar.expandElements, sep2) + + yOff = yOff - 6 + + bar.totalFS = bar:CreateFontString(nil, "OVERLAY") + bar.totalFS:SetFont(font, 10, "OUTLINE") + bar.totalFS:SetPoint("TOPLEFT", bar, "TOPLEFT", 12, yOff) + bar.totalFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + table.insert(bar.expandElements, bar.totalFS) + + bar:Hide() + FlightBar = bar +end + +local function ShowFlightBar() + CreateFlightBar() + local bar = FlightBar + + bar.srcNameFS:SetText(flightState.source) + bar.dstNameFS:SetText(flightState.dest) + + if flightState.estimated > 0 then + bar.totalFS:SetText("预计: " .. FormatTime(flightState.estimated)) + else + bar.totalFS:SetText("首次飞行 - 记录中...") + end + + bar.titleFS:SetText("飞行中...") + bar.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + bar:SetAlpha(1) + bar.compactFS:SetText("") + + -- Reset track fill + bar.trackFillFrame:SetHeight(1) + bar.progDot:ClearAllPoints() + bar.progDot:SetPoint("CENTER", bar.trackBg, "TOP", 0, 0) + bar.progGlow:ClearAllPoints() + bar.progGlow:SetPoint("CENTER", bar.progDot, "CENTER", 0, 0) + bar.elapsedFS:SetText("0:00") + bar.remainFS:SetText("") + + -- Restore collapse state + SetBarCollapsed(barCollapsed) + + bar:Show() +end + +local function UpdateFlightBar() + if not FlightBar or not FlightBar:IsShown() then return end + local bar = FlightBar + + local elapsed = GetTime() - flightState.startTime + local estimated = flightState.estimated + local progress = 0 + local compactText = FormatTime(elapsed) + + if estimated > 0 then + progress = math.min(1, elapsed / estimated) + local remain = math.max(0, estimated - elapsed) + bar.remainFS:SetText(FormatTime(remain)) + compactText = FormatTime(remain) + if remain <= 0 then + bar.remainFS:SetText("即将到达") + bar.remainFS:SetTextColor(T.arrivedText[1], T.arrivedText[2], T.arrivedText[3]) + compactText = "即将到达" + else + bar.remainFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + end + else + bar.remainFS:SetText("记录中...") + bar.remainFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + progress = math.mod(elapsed / 10, 1) * 0.8 + end + + bar.compactFS:SetText(compactText) + bar.elapsedFS:SetText("已飞行 " .. FormatTime(elapsed)) + + if not barCollapsed then + local fillH = math.max(1, progress * TRACK_H) + bar.trackFillFrame:SetHeight(fillH) + bar.progDot:ClearAllPoints() + bar.progDot:SetPoint("CENTER", bar.trackBg, "TOP", 0, -fillH) + bar.progGlow:ClearAllPoints() + bar.progGlow:SetPoint("CENTER", bar.progDot, "CENTER", 0, 0) + end +end + +local function OnFlightArrived() + if not FlightBar then return end + local bar = FlightBar + + local elapsed = GetTime() - flightState.startTime + SaveFlightTime(flightState.source, flightState.dest, elapsed, flightState.srcHash, flightState.dstHash) + + bar.titleFS:SetText("已到达!") + bar.titleFS:SetTextColor(T.arrivedText[1], T.arrivedText[2], T.arrivedText[3]) + bar.remainFS:SetText("") + bar.elapsedFS:SetText("飞行用时 " .. FormatTime(elapsed)) + bar.totalFS:SetText("") + + -- Fill track to 100% + bar.trackFillFrame:SetHeight(TRACK_H) + bar.progDot:ClearAllPoints() + bar.progDot:SetPoint("CENTER", bar.trackBg, "BOTTOM", 0, 0) + bar.progGlow:ClearAllPoints() + bar.progGlow:SetPoint("CENTER", bar.progDot, "CENTER", 0, 0) + bar.progDot:SetVertexColor(T.arrivedText[1], T.arrivedText[2], T.arrivedText[3]) + + flightState.lingerTime = 4 +end + +local function HideFlightBar() + if FlightBar then + FlightBar:Hide() + FlightBar.progDot:SetVertexColor(T.accent[1], T.accent[2], T.accent[3], T.accent[4]) + end +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function FM:Initialize() + -- TaxiMap skin updater + local updater = CreateFrame("Frame") + updater:RegisterEvent("TAXIMAP_OPENED") + updater.needsUpdate = false + + updater:SetScript("OnEvent", function() + if event == "TAXIMAP_OPENED" then + this.needsUpdate = true + end + end) + + updater:SetScript("OnUpdate", function() + if this.needsUpdate then + this.needsUpdate = false + FM:ApplySkin() + FM:UpdateDestinations() + FM:HookNodeButtons() + end + end) + + -- Hook TakeTaxiNode to capture route info before flight starts + local origTakeTaxiNode = TakeTaxiNode + TakeTaxiNode = function(index) + local numNodes = NumTaxiNodes() + local srcName = "" + local srcHash = nil + for i = 1, numNodes do + if TaxiNodeGetType(i) == "CURRENT" then + srcName = TaxiNodeName(i) + srcHash = indexToHash[i] or GetTaxiNodeHash(i) + break + end + end + local dstName = TaxiNodeName(index) or "" + local dstHash = indexToHash[index] or GetTaxiNodeHash(index) + + flightState.source = srcName + flightState.dest = dstName + flightState.srcHash = srcHash + flightState.dstHash = dstHash + flightState.pendingFlight = true + flightState.inFlight = false + flightState.lingerTime = 0 + + -- Lookup estimated time: learned DB first, then FTCData + local est = GetEstimatedTime(srcName, dstName) + if not est and srcHash and dstHash then + est = LookupFTCData(srcHash, dstHash) + end + flightState.estimated = est or 0 + + origTakeTaxiNode(index) + end + + -- Flight state monitor + local monitor = CreateFrame("Frame") + monitor:SetScript("OnUpdate", function() + local dt = arg1 or 0 + local onTaxi = UnitOnTaxi("player") + + if flightState.pendingFlight and onTaxi then + flightState.pendingFlight = false + flightState.inFlight = true + flightState.startTime = GetTime() + ShowFlightBar() + end + + if flightState.inFlight then + if not onTaxi then + flightState.inFlight = false + OnFlightArrived() + else + UpdateFlightBar() + end + end + + -- Linger after arrival then hide + if flightState.lingerTime > 0 then + flightState.lingerTime = flightState.lingerTime - dt + if flightState.lingerTime <= 1 and FlightBar then + FlightBar:SetAlpha(math.max(0, flightState.lingerTime)) + end + if flightState.lingerTime <= 0 then + flightState.lingerTime = 0 + HideFlightBar() + end + end + end) +end + +-------------------------------------------------------------------------------- +-- Bootstrap +-------------------------------------------------------------------------------- +local bootstrap = CreateFrame("Frame") +bootstrap:RegisterEvent("PLAYER_LOGIN") +bootstrap:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + if SFramesDB.enableFlightMap == nil then + SFramesDB.enableFlightMap = true + end + if SFramesDB.enableFlightMap ~= false then + FM:Initialize() + end + end +end) + +-------------------------------------------------------------------------------- +-- Debug: /ftcdebug (open taxi map first, then type this command) +-------------------------------------------------------------------------------- +SLASH_FTCDEBUG1 = "/ftcdebug" +SlashCmdList["FTCDEBUG"] = function() + local faction = GetPlayerFaction() + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightDebug]|r faction=" .. tostring(faction) .. " FTCData=" .. tostring(FTCData ~= nil)) + if FTCData then + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightDebug]|r FTCData[" .. faction .. "]=" .. tostring(FTCData[faction] ~= nil)) + end + local numNodes = NumTaxiNodes() + if not numNodes or numNodes == 0 then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[NanamiFlightDebug]|r Taxi map not open!") + return + end + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightDebug]|r Nodes=" .. numNodes .. " srcHash=" .. tostring(FM.currentSourceHash)) + for i = 1, numNodes do + local name = TaxiNodeName(i) + local ntype = TaxiNodeGetType(i) + local x, y = TaxiNodePosition(i) + local h = tostring(math.floor(x * 100000000)) + local corr = hashCorrection[h] + local corrTag = "" + if corr and corr ~= h then + corrTag = " |cFFFFFF00->fuzzy:" .. corr .. "|r" + elseif corr then + corrTag = " |cFF00FF00exact|r" + else + corrTag = " |cFFFF6666NO_MATCH|r" + end + local tag = "" + if ntype == "CURRENT" then tag = " |cFF66E666<< YOU|r" end + DEFAULT_CHAT_FRAME:AddMessage(" #" .. i .. " [" .. ntype .. "] " .. name .. " h=" .. h .. corrTag .. tag) + end +end + +-------------------------------------------------------------------------------- +-- Export: /ftcexport (prints learned flight times in FlightData.lua format) +-------------------------------------------------------------------------------- +SLASH_FTCEXPORT1 = "/ftcexport" +SlashCmdList["FTCEXPORT"] = function() + local hdb = SFramesGlobalDB and SFramesGlobalDB.flightTimesHash + if not hdb then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[NanamiFlightExport]|r No learned flight times yet.") + return + end + local count = 0 + for faction, sources in pairs(hdb) do + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightExport]|r -- " .. faction .. " learned routes:") + for srcHash, dests in pairs(sources) do + for dstHash, secs in pairs(dests) do + DEFAULT_CHAT_FRAME:AddMessage(" FTCData." .. faction .. "['" .. srcHash .. "']['" .. dstHash .. "'] = " .. secs) + count = count + 1 + end + end + end + if count == 0 then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[NanamiFlightExport]|r No learned flight times yet. Fly some routes first!") + else + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightExport]|r Total: " .. count .. " routes. Copy lines above into FlightData.lua for permanent cross-account sharing.") + end +end diff --git a/Focus.lua b/Focus.lua new file mode 100644 index 0000000..3e0fe42 --- /dev/null +++ b/Focus.lua @@ -0,0 +1,158 @@ +SFrames.Focus = {} + +local function Trim(text) + if type(text) ~= "string" then return "" end + text = string.gsub(text, "^%s+", "") + text = string.gsub(text, "%s+$", "") + return text +end + +function SFrames.Focus:EnsureDB() + if not SFramesDB then SFramesDB = {} end + if not SFramesDB.Focus then SFramesDB.Focus = {} end + return SFramesDB.Focus +end + +function SFrames.Focus:Initialize() + self:EnsureDB() +end + +function SFrames.Focus:GetFocusName() + if UnitExists and UnitExists("focus") and UnitName then + local name = UnitName("focus") + if name and name ~= "" then + return name + end + end + + local db = self:EnsureDB() + if db.name and db.name ~= "" then + return db.name + end + return nil +end + +function SFrames.Focus:SetFromTarget() + if not UnitExists or not UnitExists("target") then + return false, "NO_TARGET" + end + + local name = UnitName and UnitName("target") + if not name or name == "" then + return false, "INVALID_TARGET" + end + + local db = self:EnsureDB() + db.name = name + db.level = UnitLevel and UnitLevel("target") or nil + local _, classToken = UnitClass and UnitClass("target") + db.class = classToken + + local usedNative = false + if FocusUnit then + local ok = pcall(function() FocusUnit("target") end) + if not ok then + ok = pcall(function() FocusUnit() end) + end + if ok and UnitExists and UnitExists("focus") then + usedNative = true + end + end + + return true, name, usedNative +end + +function SFrames.Focus:Clear() + if ClearFocus then + pcall(ClearFocus) + end + + local db = self:EnsureDB() + db.name = nil + db.level = nil + db.class = nil + + return true +end + +function SFrames.Focus:Target() + if UnitExists and UnitExists("focus") then + TargetUnit("focus") + return true, "NATIVE" + end + + local name = self:GetFocusName() + if not name then + return false, "NO_FOCUS" + end + + if TargetByName then + TargetByName(name, true) + else + return false, "NO_TARGETBYNAME" + end + + if UnitExists and UnitExists("target") and UnitName and UnitName("target") == name then + return true, "NAME" + end + return false, "NOT_FOUND" +end + +function SFrames.Focus:Cast(spellName) + local spell = Trim(spellName) + if spell == "" then + return false, "NO_SPELL" + end + + if UnitExists and UnitExists("focus") then + CastSpellByName(spell) + if SpellIsTargeting and SpellIsTargeting() then + SpellTargetUnit("focus") + end + if SpellIsTargeting and SpellIsTargeting() then + SpellStopTargeting() + return false, "BAD_TARGET" + end + return true, "NATIVE" + end + + local focusName = self:GetFocusName() + if not focusName then + return false, "NO_FOCUS" + end + + local hadTarget = UnitExists and UnitExists("target") + local prevName = hadTarget and UnitName and UnitName("target") or nil + + local onFocus = false + if hadTarget and prevName == focusName then + onFocus = true + elseif TargetByName then + TargetByName(focusName, true) + if UnitExists and UnitExists("target") and UnitName and UnitName("target") == focusName then + onFocus = true + end + end + + if not onFocus then + return false, "FOCUS_NOT_FOUND" + end + + CastSpellByName(spell) + if SpellIsTargeting and SpellIsTargeting() then + SpellTargetUnit("target") + end + if SpellIsTargeting and SpellIsTargeting() then + SpellStopTargeting() + if hadTarget and prevName and prevName ~= focusName and TargetLastTarget then + TargetLastTarget() + end + return false, "BAD_TARGET" + end + + if hadTarget and prevName and prevName ~= focusName and TargetLastTarget then + TargetLastTarget() + end + return true, "NAME" +end + diff --git a/GameMenu.lua b/GameMenu.lua new file mode 100644 index 0000000..ae18581 --- /dev/null +++ b/GameMenu.lua @@ -0,0 +1,346 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Game Menu (GameMenu.lua) +-- Reskins the ESC menu (GameMenuFrame) with pink cat-paw theme +-------------------------------------------------------------------------------- + +SFrames.GameMenu = {} + +local GM = SFrames.GameMenu + +-------------------------------------------------------------------------------- +-- Theme: Pink Cat-Paw +-------------------------------------------------------------------------------- +local T = SFrames.ActiveTheme + +local BUTTON_W = 170 +local BUTTON_H = 26 +local BUTTON_GAP = 5 +local SIDE_PAD = 16 +local HEADER_H = 32 + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + local bg = bgColor or T.panelBg + local bd = borderColor or T.panelBorder + frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) + frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) +end + +local function CreateShadow(parent, size) + local s = CreateFrame("Frame", nil, parent) + local sz = size or 4 + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz) + s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0)) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.55) + s:SetBackdropBorderColor(0, 0, 0, 0.4) + return s +end + +local function HideTexture(tex) + if not tex then return end + if tex.SetTexture then tex:SetTexture(nil) end + if tex.SetAlpha then tex:SetAlpha(0) end + if tex.Hide then tex:Hide() end +end + +-------------------------------------------------------------------------------- +-- Button Styling +-------------------------------------------------------------------------------- +local MENU_BUTTON_ICONS = { + ["GameMenuButtonContinue"] = "exit", + ["GameMenuButtonOptions"] = "settings", + ["GameMenuButtonSoundOptions"] = "sound", + ["GameMenuButtonUIOptions"] = "talent", + ["GameMenuButtonKeybindings"] = "menu", + ["GameMenuButtonRatings"] = "backpack", + ["GameMenuButtonMacros"] = "ai", + ["GameMenuButtonLogout"] = "close", + ["GameMenuButtonQuit"] = "logout", +} + +local function StyleMenuButton(btn) + if not btn or btn.nanamiStyled then return end + btn.nanamiStyled = true + + HideTexture(btn:GetNormalTexture()) + HideTexture(btn:GetPushedTexture()) + HideTexture(btn:GetHighlightTexture()) + HideTexture(btn:GetDisabledTexture()) + + local name = btn:GetName() or "" + for _, suffix in ipairs({ "Left", "Right", "Middle" }) do + local tex = _G[name .. suffix] + if tex then tex:SetAlpha(0); tex:Hide() end + end + + SetRoundBackdrop(btn, T.btnBg, T.btnBorder) + btn:SetWidth(BUTTON_W) + btn:SetHeight(BUTTON_H) + + local iconKey = MENU_BUTTON_ICONS[name] + local icoSize = 12 + local gap = 4 + local fs = btn:GetFontString() + if fs then + fs:SetFont(GetFont(), 11, "OUTLINE") + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + fs:ClearAllPoints() + if iconKey and SFrames and SFrames.CreateIcon then + local ico = SFrames:CreateIcon(btn, iconKey, icoSize) + ico:SetDrawLayer("OVERLAY") + ico:SetVertexColor(T.btnText[1], T.btnText[2], T.btnText[3]) + fs:SetPoint("CENTER", btn, "CENTER", (icoSize + gap) / 2, 0) + ico:SetPoint("RIGHT", fs, "LEFT", -gap, 0) + btn.nanamiIcon = ico + else + fs:SetPoint("CENTER", btn, "CENTER", 0, 0) + end + end + + local origEnter = btn:GetScript("OnEnter") + local origLeave = btn:GetScript("OnLeave") + local origDown = btn:GetScript("OnMouseDown") + local origUp = btn:GetScript("OnMouseUp") + + btn:SetScript("OnEnter", function() + if origEnter then origEnter() end + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBorder[1], T.btnHoverBorder[2], T.btnHoverBorder[3], T.btnHoverBorder[4]) + local txt = this:GetFontString() + if txt then txt:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end + if this.nanamiIcon then this.nanamiIcon:SetVertexColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end + end) + + btn:SetScript("OnLeave", function() + if origLeave then origLeave() end + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + local txt = this:GetFontString() + if txt then txt:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end + if this.nanamiIcon then this.nanamiIcon:SetVertexColor(T.btnText[1], T.btnText[2], T.btnText[3]) end + end) + + btn:SetScript("OnMouseDown", function() + if origDown then origDown() end + this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) + end) + + btn:SetScript("OnMouseUp", function() + if origUp then origUp() end + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + end) +end + +-------------------------------------------------------------------------------- +-- Create Settings Button +-------------------------------------------------------------------------------- +local settingsBtn + +local function CreateSettingsButton(parent) + if settingsBtn then return settingsBtn end + + local btn = CreateFrame("Button", "GameMenuButtonNanamiUI", parent) + btn:SetWidth(BUTTON_W) + btn:SetHeight(BUTTON_H) + SetRoundBackdrop(btn, T.btnBg, T.btnBorder) + + local icoSize = 14 + local gap = 4 + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 11, "OUTLINE") + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + fs:SetPoint("CENTER", btn, "CENTER", (icoSize + gap) / 2, 0) + fs:SetText("Nanami-UI 设置") + + local ico = SFrames:CreateIcon(btn, "logo", icoSize) + ico:SetDrawLayer("OVERLAY") + ico:SetVertexColor(T.btnText[1], T.btnText[2], T.btnText[3]) + ico:SetPoint("RIGHT", fs, "LEFT", -gap, 0) + btn.nanamiIcon = ico + + btn:SetScript("OnClick", function() + HideUIPanel(GameMenuFrame) + if SFrames.ConfigUI and SFrames.ConfigUI.Build then + SFrames.ConfigUI:Build("ui") + end + end) + + btn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBorder[1], T.btnHoverBorder[2], T.btnHoverBorder[3], T.btnHoverBorder[4]) + fs:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + ico:SetVertexColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end) + + btn:SetScript("OnLeave", function() + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + ico:SetVertexColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end) + + btn:SetScript("OnMouseDown", function() + this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) + end) + + btn:SetScript("OnMouseUp", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + end) + + btn.nanamiStyled = true + settingsBtn = btn + return btn +end + +-------------------------------------------------------------------------------- +-- Known button priority order (covers vanilla + Turtle WoW extras) +-------------------------------------------------------------------------------- +local BUTTON_ORDER = { + "GameMenuButtonContinue", + "__NANAMI_SETTINGS__", + "GameMenuButtonOptions", + "GameMenuButtonSoundOptions", + "GameMenuButtonUIOptions", + "GameMenuButtonKeybindings", + "GameMenuButtonRatings", + "GameMenuButtonMacros", + "GameMenuButtonLogout", + "GameMenuButtonQuit", +} + +-------------------------------------------------------------------------------- +-- Frame Styling (called once at PLAYER_LOGIN, before first show) +-------------------------------------------------------------------------------- +local styled = false + +local function StyleGameMenuFrame() + if styled then return end + if not GameMenuFrame then return end + styled = true + + -- Hide all default background textures and header text + local regions = { GameMenuFrame:GetRegions() } + for _, region in ipairs(regions) do + if region then + local otype = region:GetObjectType() + if otype == "Texture" then + region:SetTexture(nil) + region:SetAlpha(0) + region:Hide() + elseif otype == "FontString" then + region:SetAlpha(0) + region:Hide() + end + end + end + + SetRoundBackdrop(GameMenuFrame, T.panelBg, T.panelBorder) + CreateShadow(GameMenuFrame, 5) + + -- Title + local title = GameMenuFrame:CreateFontString(nil, "OVERLAY") + title:SetFont(GetFont(), 13, "OUTLINE") + title:SetTextColor(T.titleColor[1], T.titleColor[2], T.titleColor[3]) + title:SetPoint("TOP", GameMenuFrame, "TOP", 0, -11) + title:SetText("Nanami-UI") + + -- Create settings button + local sBt = CreateSettingsButton(GameMenuFrame) + + -- Build a lookup of known names for quick check + local knownSet = {} + for _, name in ipairs(BUTTON_ORDER) do + if name ~= "__NANAMI_SETTINGS__" then + knownSet[name] = true + end + end + + -- Collect all child buttons that are NOT the settings button + local children = { GameMenuFrame:GetChildren() } + local unknownBtns = {} + for _, child in ipairs(children) do + if child and child:GetObjectType() == "Button" and child ~= sBt then + local cname = child:GetName() + if cname and not knownSet[cname] then + table.insert(unknownBtns, child) + end + end + end + + -- Build final ordered button list from BUTTON_ORDER + local orderedBtns = {} + for _, name in ipairs(BUTTON_ORDER) do + if name == "__NANAMI_SETTINGS__" then + table.insert(orderedBtns, sBt) + else + local btn = _G[name] + if btn then + StyleMenuButton(btn) + table.insert(orderedBtns, btn) + end + end + end + + -- Append unknown / Turtle WoW extra buttons before Logout & Quit + local insertBefore = table.getn(orderedBtns) + for i = table.getn(orderedBtns), 1, -1 do + local bname = orderedBtns[i]:GetName() or "" + if bname == "GameMenuButtonLogout" or bname == "GameMenuButtonQuit" then + insertBefore = i + else + break + end + end + for _, btn in ipairs(unknownBtns) do + StyleMenuButton(btn) + table.insert(orderedBtns, insertBefore, btn) + insertBefore = insertBefore + 1 + end + + -- Layout vertically + local numBtns = table.getn(orderedBtns) + local totalH = numBtns * BUTTON_H + (numBtns - 1) * BUTTON_GAP + local frameH = HEADER_H + SIDE_PAD + totalH + SIDE_PAD + local frameW = BUTTON_W + SIDE_PAD * 2 + + GameMenuFrame:SetWidth(frameW) + GameMenuFrame:SetHeight(frameH) + + local startY = -(HEADER_H + SIDE_PAD) + for i, btn in ipairs(orderedBtns) do + btn:ClearAllPoints() + btn:SetPoint("TOP", GameMenuFrame, "TOP", 0, startY - (i - 1) * (BUTTON_H + BUTTON_GAP)) + end +end + +-------------------------------------------------------------------------------- +-- Hook: style at login, BEFORE any show (avoids OnShow size-change issues) +-------------------------------------------------------------------------------- +local hookFrame = CreateFrame("Frame") +hookFrame:RegisterEvent("PLAYER_LOGIN") +hookFrame:SetScript("OnEvent", function() + if GameMenuFrame then + StyleGameMenuFrame() + end +end) diff --git a/GearScore.lua b/GearScore.lua new file mode 100644 index 0000000..b9ea3e7 --- /dev/null +++ b/GearScore.lua @@ -0,0 +1,1133 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: GearScore (GearScore.lua) +-- 基于乌龟服(Turtle WoW)属性收益理论的装备评分系统 +-- 评分 1-10: 衡量该装备对当前职业各天赋的适配度 +-- 考虑: 属性权重 × 装备类型兼容性 × 等级适配 +-------------------------------------------------------------------------------- + +SFrames.GearScore = {} +local GS = SFrames.GearScore + +-------------------------------------------------------------------------------- +-- Item budget cost per unit of each stat (for normalization) +-- Higher cost = stat is "rarer/more expensive" per item budget point +-------------------------------------------------------------------------------- + +local BUDGET_COST = { + STR = 1.0, AGI = 1.0, STA = 1.0, INT = 1.0, SPI = 1.0, + CRIT = 14.0, TOHIT = 15.0, RANGEDCRIT = 14.0, + SPELLCRIT = 14.0, SPELLTOHIT = 15.0, + ATTACKPOWER = 0.5, RANGEDATTACKPOWER = 0.5, + DMG = 0.86, HEAL = 0.58, + DEFENSE = 1.5, DODGE = 12.0, PARRY = 12.0, BLOCK = 12.0, + BLOCKVALUE = 0.5, ARMOR = 0.05, BASEARMOR = 0.02, + HEALTHREG = 2.0, MANAREG = 2.0, + HEALTH = 0.07, MANA = 0.07, + WEAPONDPS = 3.0, +} + +-------------------------------------------------------------------------------- +-- EP Weight Tables (tab = talent tab index for primary spec detection) +-------------------------------------------------------------------------------- + +local WEIGHTS = { + WARRIOR = { + specs = { + { name = "武器", tab = 1, color = "ffC79C6E", + w = { STR=2.0, AGI=1.4, STA=0.1, TOHIT=18, CRIT=25, ATTACKPOWER=1.0, + HEALTH=0.1, WEAPONDPS=14, BASEARMOR=0.01 } }, + { name = "狂怒", tab = 2, color = "ffC79C6E", + w = { STR=2.2, AGI=1.6, STA=0.1, TOHIT=20, CRIT=22, ATTACKPOWER=1.0, + HEALTH=0.1, WEAPONDPS=12, BASEARMOR=0.01 } }, + { name = "防护", tab = 3, color = "ff69CCF0", + w = { STR=1.0, AGI=1.8, STA=2.5, TOHIT=10, CRIT=3, ATTACKPOWER=0.5, + DEFENSE=1.5, DODGE=12, PARRY=12, BLOCK=8, BLOCKVALUE=0.5, + ARMOR=0.12, HEALTH=0.25, HEALTHREG=0.5, WEAPONDPS=4, BASEARMOR=0.05 } }, + }, + -- 硬核物理(战士型): 耐 > 力 > 敏 > 攻强 + hc = { name = "硬核", color = "ffFF4444", + w = { STA=3.0, STR=2.0, AGI=1.5, ATTACKPOWER=1.0, + TOHIT=5, CRIT=5, DEFENSE=1.0, DODGE=8, PARRY=5, BLOCK=4, BLOCKVALUE=0.3, + ARMOR=0.08, HEALTH=0.2, HEALTHREG=2.0, WEAPONDPS=5, BASEARMOR=0.03 } }, + }, + PALADIN = { + specs = { + { name = "神圣", tab = 1, color = "ff00FF96", + w = { INT=0.35, SPI=0.4, STA=0.05, HEAL=1.0, DMG=0.3, SPELLCRIT=6, MANAREG=3.0, + MANA=0.02, BASEARMOR=0.005 } }, + { name = "防护", tab = 2, color = "ff69CCF0", + w = { STR=1.2, AGI=1.0, STA=2.5, INT=0.3, TOHIT=10, CRIT=5, ATTACKPOWER=0.5, DMG=0.4, + DEFENSE=1.5, DODGE=12, PARRY=12, BLOCK=10, BLOCKVALUE=0.5, + ARMOR=0.12, HEALTH=0.25, MANAREG=1.5, WEAPONDPS=3, BASEARMOR=0.05 } }, + { name = "惩戒", tab = 3, color = "ffF58CBA", + w = { STR=2.0, AGI=1.0, STA=0.1, INT=0.25, TOHIT=16, CRIT=20, SPELLCRIT=10, + ATTACKPOWER=1.0, DMG=0.6, HEAL=0.05, WEAPONDPS=12, BASEARMOR=0.01 } }, + }, + -- 硬核圣骑士: 耐 > 力 > 智 = 敏 = 精 + hc = { name = "硬核", color = "ffFF4444", + w = { STA=3.0, STR=2.0, INT=1.5, AGI=1.5, SPI=1.5, + TOHIT=5, CRIT=5, ATTACKPOWER=0.5, HEAL=0.8, DMG=0.3, + DEFENSE=0.8, DODGE=5, ARMOR=0.06, HEALTH=0.2, + HEALTHREG=1.5, MANAREG=1.5, WEAPONDPS=4, BASEARMOR=0.03 } }, + }, + HUNTER = { + specs = { + { name = "野兽", tab = 1, color = "ffABD473", + w = { AGI=2.4, STR=0.3, STA=0.1, INT=0.2, TOHIT=18, CRIT=22, RANGEDCRIT=22, + ATTACKPOWER=0.8, RANGEDATTACKPOWER=1.0, MANAREG=1.5, WEAPONDPS=10, BASEARMOR=0.005 } }, + { name = "射击", tab = 2, color = "ffABD473", + w = { AGI=2.4, STR=0.3, STA=0.1, INT=0.2, TOHIT=18, CRIT=22, RANGEDCRIT=22, + ATTACKPOWER=0.8, RANGEDATTACKPOWER=1.0, MANAREG=1.5, WEAPONDPS=10, BASEARMOR=0.005 } }, + { name = "生存", tab = 3, color = "ffABD473", + w = { AGI=2.0, STR=0.8, STA=0.3, INT=0.2, SPI=0.3, TOHIT=16, CRIT=18, RANGEDCRIT=18, + ATTACKPOWER=1.0, RANGEDATTACKPOWER=1.0, MANAREG=1.0, WEAPONDPS=8, BASEARMOR=0.008 } }, + }, + -- 硬核猎人: 耐 > 敏 > 智 > 力 + hc = { name = "硬核", color = "ffFF4444", + w = { STA=3.0, AGI=2.0, INT=1.0, STR=0.5, SPI=1.5, + TOHIT=5, CRIT=5, RANGEDCRIT=5, ATTACKPOWER=0.4, RANGEDATTACKPOWER=0.5, + ARMOR=0.06, HEALTH=0.2, HEALTHREG=2.5, WEAPONDPS=3, BASEARMOR=0.02 } }, + }, + ROGUE = { + specs = { + { name = "刺杀", tab = 1, color = "ffFFF569", + w = { AGI=2.0, STR=1.0, STA=0.1, TOHIT=18, CRIT=25, ATTACKPOWER=1.0, + WEAPONDPS=10, BASEARMOR=0.008 } }, + { name = "战斗", tab = 2, color = "ffFFF569", + w = { AGI=2.0, STR=1.0, STA=0.1, TOHIT=20, CRIT=22, ATTACKPOWER=1.0, + WEAPONDPS=14, BASEARMOR=0.008 } }, + { name = "敏锐", tab = 3, color = "ffFFF569", + w = { AGI=2.2, STR=1.0, STA=0.5, TOHIT=16, CRIT=20, ATTACKPOWER=1.0, DODGE=5, + WEAPONDPS=8, BASEARMOR=0.01 } }, + }, + -- 硬核潜行者: 耐 > 敏 > 力 > 攻强 + hc = { name = "硬核", color = "ffFF4444", + w = { STA=3.0, AGI=2.5, STR=1.5, ATTACKPOWER=1.0, + TOHIT=5, CRIT=5, DODGE=5, ARMOR=0.06, + HEALTH=0.2, HEALTHREG=2.0, WEAPONDPS=5, BASEARMOR=0.02 } }, + }, + PRIEST = { + specs = { + { name = "戒律", tab = 1, color = "ff00FF96", + w = { INT=0.35, SPI=0.55, STA=0.05, HEAL=1.0, DMG=0.3, SPELLCRIT=5, MANAREG=3.0, + MANA=0.02, BASEARMOR=0.003 } }, + { name = "神圣", tab = 2, color = "ff00FF96", + w = { INT=0.35, SPI=0.55, STA=0.05, HEAL=1.0, DMG=0.3, SPELLCRIT=5, MANAREG=3.0, + MANA=0.02, BASEARMOR=0.003 } }, + { name = "暗影", tab = 3, color = "ff9482C9", + w = { INT=0.15, SPI=0.35, STA=0.05, DMG=1.0, HEAL=0.05, SPELLCRIT=6, SPELLTOHIT=14, + MANAREG=1.5, BASEARMOR=0.003 } }, + }, + -- 硬核法系(牧师): 耐 > 智 = 治疗 > 精 + hc = { name = "硬核", color = "ffFF4444", + w = { STA=3.0, INT=2.0, HEAL=2.0, DMG=1.5, SPI=1.5, + SPELLCRIT=3, SPELLTOHIT=5, MANAREG=2.5, + ARMOR=0.06, HEALTH=0.15, HEALTHREG=1.0, BASEARMOR=0.02 } }, + }, + SHAMAN = { + specs = { + { name = "元素", tab = 1, color = "ff0070DE", + w = { INT=0.2, SPI=0.1, STA=0.05, DMG=1.0, SPELLCRIT=8, SPELLTOHIT=14, + ATTACKPOWER=0.1, MANAREG=2.0, BASEARMOR=0.005 } }, + { name = "增强", tab = 2, color = "ff0070DE", + w = { STR=2.0, AGI=1.6, STA=0.1, INT=0.2, TOHIT=18, CRIT=22, ATTACKPOWER=1.0, + DMG=0.3, SPELLCRIT=5, MANAREG=1.0, WEAPONDPS=14, BASEARMOR=0.01 } }, + { name = "恢复", tab = 3, color = "ff00FF96", + w = { INT=0.35, SPI=0.3, STA=0.05, HEAL=1.0, DMG=0.2, SPELLCRIT=5, MANAREG=3.5, + MANA=0.02, BASEARMOR=0.005 } }, + }, + -- 硬核萨满(混合): 耐 > 力=敏 > 智=精 + hc = { name = "硬核", color = "ffFF4444", + w = { STA=3.0, STR=1.5, AGI=1.5, INT=1.0, SPI=1.0, + TOHIT=5, CRIT=5, ATTACKPOWER=0.5, HEAL=0.8, DMG=0.5, + MANAREG=2.0, ARMOR=0.06, HEALTH=0.2, HEALTHREG=1.5, + WEAPONDPS=4, BASEARMOR=0.02 } }, + }, + MAGE = { + specs = { + { name = "奥术", tab = 1, color = "ff69CCF0", + w = { INT=0.2, SPI=0.15, STA=0.4, DMG=1.0, SPELLCRIT=7, SPELLTOHIT=14, MANAREG=2.0, + MANA=0.01, BASEARMOR=0.003 } }, + { name = "火焰", tab = 2, color = "ff69CCF0", + w = { INT=0.15, SPI=0.1, STA=0.4, DMG=1.0, SPELLCRIT=9, SPELLTOHIT=14, MANAREG=2.0, + MANA=0.01, BASEARMOR=0.003 } }, + { name = "冰霜", tab = 3, color = "ff69CCF0", + w = { INT=0.15, SPI=0.1, STA=0.4, DMG=1.0, SPELLCRIT=7, SPELLTOHIT=14, MANAREG=2.0, + MANA=0.01, BASEARMOR=0.003 } }, + }, + -- 硬核法系(法师): 耐 > 智 = 法伤 > 精 + hc = { name = "硬核", color = "ffFF4444", + w = { STA=3.0, INT=2.0, DMG=2.0, SPI=1.5, + SPELLCRIT=3, SPELLTOHIT=5, MANAREG=2.5, + ARMOR=0.08, HEALTH=0.15, HEALTHREG=1.0, BASEARMOR=0.02 } }, + }, + WARLOCK = { + specs = { + { name = "痛苦", tab = 1, color = "ff9482C9", + w = { INT=0.15, SPI=0.1, STA=0.4, DMG=1.0, SPELLCRIT=4, SPELLTOHIT=14, MANAREG=1.0, + HEALTH=0.08, BASEARMOR=0.003 } }, + { name = "恶魔", tab = 2, color = "ff9482C9", + w = { INT=0.15, SPI=0.1, STA=0.4, DMG=1.0, SPELLCRIT=6, SPELLTOHIT=14, MANAREG=1.5, + BASEARMOR=0.003 } }, + { name = "毁灭", tab = 3, color = "ff9482C9", + w = { INT=0.15, SPI=0.1, STA=0.4, DMG=1.0, SPELLCRIT=8, SPELLTOHIT=14, MANAREG=1.5, + BASEARMOR=0.003 } }, + }, + -- 硬核法系(术士): 耐 > 智 = 法伤 > 精 + hc = { name = "硬核", color = "ffFF4444", + w = { STA=3.0, INT=2.0, DMG=2.0, SPI=1.0, + SPELLCRIT=3, SPELLTOHIT=5, MANAREG=2.0, + ARMOR=0.06, HEALTH=0.15, HEALTHREG=1.0, BASEARMOR=0.02 } }, + }, + DRUID = { + specs = { + { name = "平衡", tab = 1, color = "ffFF7D0A", + w = { INT=0.2, SPI=0.15, STA=0.05, DMG=1.0, HEAL=0.1, SPELLCRIT=7, SPELLTOHIT=14, + MANAREG=2.0, MANA=0.01, BASEARMOR=0.005 } }, + { name = "野猫", tab = 2, color = "ffFF7D0A", + w = { STR=2.4, AGI=1.4, STA=0.1, TOHIT=18, CRIT=22, ATTACKPOWER=1.0, DODGE=2, + WEAPONDPS=2, BASEARMOR=0.008 } }, + { name = "野熊", tab = 2, color = "ff69CCF0", + w = { STR=1.5, AGI=2.0, STA=2.5, TOHIT=8, CRIT=5, ATTACKPOWER=0.5, + DEFENSE=1.2, DODGE=12, ARMOR=0.12, HEALTH=0.25, BASEARMOR=0.05 } }, + { name = "恢复", tab = 3, color = "ff00FF96", + w = { INT=0.35, SPI=0.45, STA=0.05, HEAL=1.0, DMG=0.2, SPELLCRIT=5, MANAREG=3.0, + MANA=0.02, BASEARMOR=0.005 } }, + }, + -- 硬核德鲁伊(混合偏生存): 耐 > 敏 > 力 > 智 = 精 + hc = { name = "硬核", color = "ffFF4444", + w = { STA=3.0, AGI=2.0, STR=1.5, INT=1.0, SPI=1.0, + TOHIT=5, CRIT=5, ATTACKPOWER=0.5, HEAL=0.8, DMG=0.5, + DODGE=5, ARMOR=0.06, HEALTH=0.2, HEALTHREG=2.0, + WEAPONDPS=2, BASEARMOR=0.02 } }, + }, +} + +-------------------------------------------------------------------------------- +-- Armor type compatibility (class × armor subclass × level) +-- 1.0 = ideal, 0 = should never wear this +-------------------------------------------------------------------------------- + +local ARMOR_COMPAT = { + WARRIOR = { Plate=1.0, Mail=0.3, Leather=0.1, Cloth=0.05 }, + PALADIN = { Plate=1.0, Mail=0.3, Leather=0.1, Cloth=0.05 }, + HUNTER = { Mail=1.0, Leather=0.5, Cloth=0.1 }, + SHAMAN = { Mail=1.0, Leather=0.5, Cloth=0.1 }, + ROGUE = { Leather=1.0, Cloth=0.3 }, + DRUID = { Leather=1.0, Cloth=0.3 }, + PRIEST = { Cloth=1.0 }, + MAGE = { Cloth=1.0 }, + WARLOCK = { Cloth=1.0 }, +} + +local ARMOR_COMPAT_LOW = { + WARRIOR = { Mail=1.0, Leather=0.6, Cloth=0.1 }, + PALADIN = { Mail=1.0, Leather=0.6, Cloth=0.1 }, + HUNTER = { Leather=1.0, Cloth=0.3 }, + SHAMAN = { Leather=1.0, Cloth=0.3 }, + ROGUE = { Leather=1.0, Cloth=0.3 }, + DRUID = { Leather=1.0, Cloth=0.3 }, + PRIEST = { Cloth=1.0 }, + MAGE = { Cloth=1.0 }, + WARLOCK = { Cloth=1.0 }, +} + +-------------------------------------------------------------------------------- +-- Equipment slot compatibility per spec (CLASS_specIdx) +-- 2H weapons for tanks = 0.05, shields for DPS = low, etc. +-- Missing entries default to 1.0 +-------------------------------------------------------------------------------- + +local SLOT_COMPAT = { + WARRIOR_1 = { INVTYPE_SHIELD = 0.1, INVTYPE_HOLDABLE = 0.05 }, + WARRIOR_2 = { INVTYPE_SHIELD = 0.1, INVTYPE_HOLDABLE = 0.05 }, + WARRIOR_3 = { INVTYPE_2HWEAPON = 0.05, INVTYPE_HOLDABLE = 0.05 }, + + PALADIN_1 = { INVTYPE_2HWEAPON = 0.3 }, + PALADIN_2 = { INVTYPE_2HWEAPON = 0.05 }, + PALADIN_3 = { INVTYPE_SHIELD = 0.2, INVTYPE_HOLDABLE = 0.1 }, + + HUNTER_1 = { INVTYPE_SHIELD = 0 }, + HUNTER_2 = { INVTYPE_SHIELD = 0 }, + HUNTER_3 = { INVTYPE_SHIELD = 0 }, + + ROGUE_1 = { INVTYPE_2HWEAPON = 0, INVTYPE_SHIELD = 0, INVTYPE_HOLDABLE = 0 }, + ROGUE_2 = { INVTYPE_2HWEAPON = 0, INVTYPE_SHIELD = 0, INVTYPE_HOLDABLE = 0 }, + ROGUE_3 = { INVTYPE_2HWEAPON = 0, INVTYPE_SHIELD = 0, INVTYPE_HOLDABLE = 0 }, + + SHAMAN_1 = { INVTYPE_2HWEAPON = 0.3 }, + SHAMAN_2 = { INVTYPE_SHIELD = 0.3 }, + + DRUID_1 = { INVTYPE_SHIELD = 0 }, + DRUID_2 = { INVTYPE_SHIELD = 0, INVTYPE_HOLDABLE = 0 }, + DRUID_3 = { INVTYPE_SHIELD = 0, INVTYPE_HOLDABLE = 0 }, + DRUID_4 = { INVTYPE_SHIELD = 0 }, + + MAGE_1 = { INVTYPE_SHIELD = 0 }, + MAGE_2 = { INVTYPE_SHIELD = 0 }, + MAGE_3 = { INVTYPE_SHIELD = 0 }, + WARLOCK_1 = { INVTYPE_SHIELD = 0 }, + WARLOCK_2 = { INVTYPE_SHIELD = 0 }, + WARLOCK_3 = { INVTYPE_SHIELD = 0 }, + PRIEST_1 = { INVTYPE_SHIELD = 0 }, + PRIEST_2 = { INVTYPE_SHIELD = 0 }, + PRIEST_3 = { INVTYPE_SHIELD = 0 }, +} + +-------------------------------------------------------------------------------- +-- Level-based weight adjustments +-------------------------------------------------------------------------------- + +local function AdjustWeightsForLevel(baseWeights, level, isHC) + if level >= 55 then return baseWeights end + local adj = {} + for k, v in pairs(baseWeights) do adj[k] = v end + + if isHC then + -- HC: STA stays high at all levels (survival is the point) + -- Only reduce hit/crit which are less useful at low levels + if level <= 20 then + adj.SPI = (adj.SPI or 0) * 1.3 + adj.TOHIT = (adj.TOHIT or 0) * 0.2 + adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.2 + adj.HEALTHREG = (adj.HEALTHREG or 0) + 1.0 + adj.ARMOR = (adj.ARMOR or 0) + 0.03 + elseif level <= 40 then + adj.SPI = (adj.SPI or 0) * 1.15 + adj.ARMOR = (adj.ARMOR or 0) + 0.02 + adj.TOHIT = (adj.TOHIT or 0) * 0.5 + adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.5 + else + adj.TOHIT = (adj.TOHIT or 0) * 0.8 + adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.8 + end + else + if level <= 20 then + adj.SPI = (adj.SPI or 0) * 2.5 + 0.5 + adj.STA = (adj.STA or 0) * 0.3 + adj.TOHIT = (adj.TOHIT or 0) * 0.2 + adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.2 + adj.HEALTHREG = (adj.HEALTHREG or 0) + 1.5 + adj.ARMOR = (adj.ARMOR or 0) + 0.03 + elseif level <= 40 then + adj.SPI = (adj.SPI or 0) * 1.5 + 0.2 + adj.ARMOR = (adj.ARMOR or 0) + 0.02 + adj.TOHIT = (adj.TOHIT or 0) * 0.6 + adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.6 + else + adj.TOHIT = (adj.TOHIT or 0) * 0.85 + adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.85 + end + end + return adj +end + +-------------------------------------------------------------------------------- +-- Player info helpers +-------------------------------------------------------------------------------- + +local function GetPlayerClassToken() + local _, classEN = UnitClass("player") + if classEN then return string.upper(classEN) end + return nil +end + +local function GetPlayerLevel() + return UnitLevel("player") or 60 +end + +local function GetPrimaryTabIndex() + if not GetNumTalentTabs then return 1 end + local maxPts, maxIdx = 0, 1 + for i = 1, GetNumTalentTabs() do + local _, _, pts = GetTalentTabInfo(i) + if pts and pts > maxPts then + maxPts = pts + maxIdx = i + end + end + return maxIdx +end + +local function IsPlayerHardcore() + if IsHardcore and IsHardcore("player") then return true end + if C_TurtleWoW and C_TurtleWoW.IsHardcore and C_TurtleWoW.IsHardcore("player") then + return true + end + return false +end + +-------------------------------------------------------------------------------- +-- Item link/info utilities +-------------------------------------------------------------------------------- + +local function ExtractItemString(link) + if not link then return nil end + local _, _, itemStr = string.find(link, "(item:[%-?%d:]+)") + return itemStr +end + +-- Item quality detection from link color code +-- WoW link format: |cffXXXXXX|Hitem:...|h[Name]|h|r +local QUALITY_FROM_COLOR = { + ["9d9d9d"] = 0, -- Poor (gray) + ["ffffff"] = 1, -- Common (white) + ["1eff00"] = 2, -- Uncommon (green) + ["0070dd"] = 3, -- Rare (blue) + ["a335ee"] = 4, -- Epic (purple) + ["ff8000"] = 5, -- Legendary (orange) + ["e6cc80"] = 6, -- Artifact +} + +local QUALITY_MULT = { + [0] = 0, -- Poor: skip entirely + [1] = 0.50, -- Common: penalty + [2] = 0.90, -- Uncommon: slight penalty + [3] = 0.95, -- Rare: near full + [4] = 1.0, -- Epic: full + [5] = 1.0, -- Legendary: full + [6] = 1.0, -- Artifact: full +} + +local function GetQualityFromLink(link) + if not link then return -1 end + local _, _, hex = string.find(link, "|cff(%x%x%x%x%x%x)") + if hex then + return QUALITY_FROM_COLOR[string.lower(hex)] or 1 + end + return 1 +end + +local function GetQualityFromTooltip(tooltip) + if not tooltip then return 1 end + local tipName = tooltip:GetName() + if not tipName then return 1 end + local nameObj = getglobal(tipName .. "TextLeft1") + if not nameObj then return 1 end + local r, g, b = nameObj:GetTextColor() + if not r then return 1 end + -- Gray: r≈0.62, g≈0.62, b≈0.62 + if r < 0.7 and g < 0.7 and b < 0.7 then return 0 end + -- White: r=1, g=1, b=1 + if r > 0.95 and g > 0.95 and b > 0.95 then return 1 end + -- Green: r≈0.12, g=1, b=0 + if g > 0.9 and r < 0.2 then return 2 end + -- Blue: r=0, g≈0.44, b≈0.87 + if b > 0.8 and r < 0.1 then return 3 end + -- Purple: r≈0.64, g≈0.21, b≈0.93 + if b > 0.85 and r > 0.5 then return 4 end + -- Orange: r=1, g≈0.5, b=0 + if r > 0.9 and g > 0.4 and b < 0.1 then return 5 end + return 1 +end + +-- Normalize item class/subclass strings (Chinese client support) +local ARMOR_CLASS_NAMES = { Armor=true, ["护甲"]=true, ["铠甲"]=true } +local ARMOR_MISC_NAMES = { Miscellaneous=true, ["其它"]=true, ["杂项"]=true, ["其他"]=true } +local SHIELD_NAMES = { Shield=true, Shields=true, ["盾牌"]=true, ["盾"]=true } +local SUBCLASS_NORMALIZE = { + Plate="Plate", ["板甲"]="Plate", ["铠甲"]="Plate", + Mail="Mail", ["锁甲"]="Mail", ["链甲"]="Mail", + Leather="Leather", ["皮甲"]="Leather", ["皮革"]="Leather", + Cloth="Cloth", ["布甲"]="Cloth", ["布料"]="Cloth", +} + +-- Parse equipLoc and armor type directly from tooltip text +local SLOT_TEXT_TO_INVTYPE = { + ["Head"]="INVTYPE_HEAD", ["头部"]="INVTYPE_HEAD", + ["Neck"]="INVTYPE_NECK", ["颈部"]="INVTYPE_NECK", + ["Shoulder"]="INVTYPE_SHOULDER", ["肩部"]="INVTYPE_SHOULDER", + ["Shirt"]="INVTYPE_BODY", ["衬衣"]="INVTYPE_BODY", + ["Chest"]="INVTYPE_CHEST", ["胸部"]="INVTYPE_CHEST", + ["Robe"]="INVTYPE_ROBE", ["胸甲"]="INVTYPE_ROBE", + ["Waist"]="INVTYPE_WAIST", ["腰部"]="INVTYPE_WAIST", + ["Legs"]="INVTYPE_LEGS", ["腿部"]="INVTYPE_LEGS", + ["Feet"]="INVTYPE_FEET", ["脚"]="INVTYPE_FEET", ["足"]="INVTYPE_FEET", + ["Wrist"]="INVTYPE_WRIST", ["手腕"]="INVTYPE_WRIST", + ["Hands"]="INVTYPE_HAND", ["手"]="INVTYPE_HAND", + ["Finger"]="INVTYPE_FINGER", ["手指"]="INVTYPE_FINGER", + ["Trinket"]="INVTYPE_TRINKET", ["饰品"]="INVTYPE_TRINKET", + ["Back"]="INVTYPE_CLOAK", ["背部"]="INVTYPE_CLOAK", + ["Main Hand"]="INVTYPE_WEAPONMAINHAND", ["主手"]="INVTYPE_WEAPONMAINHAND", + ["One-Hand"]="INVTYPE_WEAPON", ["单手"]="INVTYPE_WEAPON", + ["Off Hand"]="INVTYPE_WEAPONOFFHAND", ["副手"]="INVTYPE_WEAPONOFFHAND", + ["Two-Hand"]="INVTYPE_2HWEAPON", ["双手"]="INVTYPE_2HWEAPON", + ["Held In Off-hand"]="INVTYPE_HOLDABLE", ["副手物品"]="INVTYPE_HOLDABLE", + ["Ranged"]="INVTYPE_RANGED", ["远程"]="INVTYPE_RANGED", + ["Thrown"]="INVTYPE_THROWN", ["投掷"]="INVTYPE_THROWN", + ["Relic"]="INVTYPE_RELIC", ["圣物"]="INVTYPE_RELIC", + ["Shield"]="INVTYPE_SHIELD", ["盾牌"]="INVTYPE_SHIELD", +} + +local function ParseEquipLocFromTooltip(tooltip) + if not tooltip then return nil, nil, nil end + local tipName = tooltip:GetName() + if not tipName then return nil, nil, nil end + local numLines = tooltip:NumLines() + if not numLines or numLines < 2 then return nil, nil, nil end + + local foundEquipLoc, foundArmorType, foundItemClass + for i = 2, math.min(numLines, 8) do + local leftObj = getglobal(tipName .. "TextLeft" .. i) + local rightObj = getglobal(tipName .. "TextRight" .. i) + if leftObj then + local leftText = leftObj:GetText() + if leftText and leftText ~= "" then + local invtype = SLOT_TEXT_TO_INVTYPE[leftText] + if invtype then + foundEquipLoc = invtype + if rightObj then + local rightText = rightObj:GetText() + if rightText and rightText ~= "" then + foundArmorType = rightText + if ARMOR_CLASS_NAMES[rightText] or SUBCLASS_NORMALIZE[rightText] + or SHIELD_NAMES[rightText] then + foundItemClass = "Armor" + end + end + end + break + end + end + end + end + return foundEquipLoc, foundArmorType, foundItemClass +end + +local GS_EQUIP_LOCS = { + INVTYPE_HEAD=true, INVTYPE_NECK=true, INVTYPE_SHOULDER=true, + INVTYPE_BODY=true, INVTYPE_CHEST=true, INVTYPE_ROBE=true, + INVTYPE_WAIST=true, INVTYPE_LEGS=true, INVTYPE_FEET=true, + INVTYPE_WRIST=true, INVTYPE_HAND=true, + INVTYPE_FINGER=true, INVTYPE_TRINKET=true, + INVTYPE_CLOAK=true, + INVTYPE_WEAPON=true, INVTYPE_2HWEAPON=true, INVTYPE_WEAPONMAINHAND=true, + INVTYPE_SHIELD=true, INVTYPE_WEAPONOFFHAND=true, INVTYPE_HOLDABLE=true, + INVTYPE_RANGED=true, INVTYPE_RANGEDRIGHT=true, INVTYPE_THROWN=true, + INVTYPE_RELIC=true, +} + +-------------------------------------------------------------------------------- +-- Tooltip text parser (zero external dependency) +-------------------------------------------------------------------------------- + +local STAT_PATTERNS = { + -- Weapon DPS (float, e.g. "(每秒伤害19.7)" or "(12.3 damage per second)") + { p = "%((%d+%.?%d*) damage per second%)", s = "WEAPONDPS" }, + { p = "每秒伤害(%d+%.?%d*)", s = "WEAPONDPS" }, + { p = "每秒(%d+%.?%d*)点伤害", s = "WEAPONDPS" }, + + -- Base armor value ("63点护甲" / "123 护甲" / "500 Armor", no + prefix) + { p = "^(%d+)点护甲", s = "BASEARMOR" }, + { p = "^(%d+) 点护甲", s = "BASEARMOR" }, + { p = "^(%d+) 护甲$", s = "BASEARMOR" }, + { p = "^(%d+) Armor$", s = "BASEARMOR" }, + + -- Bonus armor ("+30 Armor" / "+30 护甲" from enchants) + { p = "%+(%d+) Armor", s = "ARMOR" }, + { p = "%+(%d+) 护甲", s = "ARMOR" }, + { p = "护甲值提高(%d+)", s = "ARMOR" }, + + -- Primary stats + { p = "%+(%d+) Strength", s = "STR" }, + { p = "%+(%d+) Agility", s = "AGI" }, + { p = "%+(%d+) Stamina", s = "STA" }, + { p = "%+(%d+) Intellect", s = "INT" }, + { p = "%+(%d+) Spirit", s = "SPI" }, + { p = "%+(%d+) 力量", s = "STR" }, + { p = "%+(%d+) 敏捷", s = "AGI" }, + { p = "%+(%d+) 耐力", s = "STA" }, + { p = "%+(%d+) 智力", s = "INT" }, + { p = "%+(%d+) 精神", s = "SPI" }, + + -- Crit (spell > ranged > melee, more specific patterns first) + { p = "critical strike with spells by (%d+)%%", s = "SPELLCRIT" }, + { p = "法术暴击.-(%d+)%%", s = "SPELLCRIT" }, + { p = "法术.-致命一击.-(%d+)%%", s = "SPELLCRIT" }, + { p = "critical strike with ranged weapons by (%d+)%%", s = "RANGEDCRIT" }, + { p = "远程暴击.-(%d+)%%", s = "RANGEDCRIT" }, + { p = "critical strike by (%d+)%%", s = "CRIT" }, + { p = "致命一击.-(%d+)%%", s = "CRIT" }, + { p = "暴击.-(%d+)%%", s = "CRIT" }, + + -- Hit + { p = "hit with spells by (%d+)%%", s = "SPELLTOHIT" }, + { p = "法术命中.-(%d+)%%", s = "SPELLTOHIT" }, + { p = "chance to hit by (%d+)%%", s = "TOHIT" }, + { p = "命中.-(%d+)%%", s = "TOHIT" }, + + -- Attack Power + { p = "%+(%d+) ranged Attack Power", s = "RANGEDATTACKPOWER" }, + { p = "%+(%d+) 远程攻击强度", s = "RANGEDATTACKPOWER" }, + { p = "远程攻击强度提高(%d+)", s = "RANGEDATTACKPOWER" }, + { p = "%+(%d+) Attack Power", s = "ATTACKPOWER" }, + { p = "%+(%d+) 攻击强度", s = "ATTACKPOWER" }, + { p = "攻击强度提高(%d+)", s = "ATTACKPOWER" }, + + -- Spell damage + healing (combined, MUST be before individual patterns) + { p = "damage and healing done by magical spells and effects by up to (%d+)", s = "DMG", a = "HEAL" }, + { p = "伤害和治疗效果.-最多(%d+)", s = "DMG", a = "HEAL" }, + { p = "法术伤害和治疗.-(%d+)", s = "DMG", a = "HEAL" }, + + -- Healing only + { p = "healing done by spells and effects by up to (%d+)", s = "HEAL" }, + { p = "Increases healing done by up to (%d+)", s = "HEAL" }, + { p = "治疗效果.-最多(%d+)", s = "HEAL" }, + { p = "治疗法术.-最多(%d+)", s = "HEAL" }, + { p = "治疗量.-最多(%d+)", s = "HEAL" }, + + -- Spell damage only + { p = "damage done by magical spells and effects by up to (%d+)", s = "DMG" }, + { p = "法术伤害.-最多(%d+)", s = "DMG" }, + { p = "魔法伤害.-最多(%d+)", s = "DMG" }, + + -- Mana/health regen + { p = "Restores (%d+) mana per 5 sec", s = "MANAREG" }, + { p = "(%d+) mana per 5 sec", s = "MANAREG" }, + { p = "每5秒恢复(%d+)点法力", s = "MANAREG" }, + { p = "每5秒回复(%d+)点法力", s = "MANAREG" }, + { p = "5秒回复(%d+)点法力", s = "MANAREG" }, + { p = "Restores (%d+) health per 5 sec", s = "HEALTHREG" }, + { p = "每5秒恢复(%d+)点生命", s = "HEALTHREG" }, + { p = "每5秒回复(%d+)点生命", s = "HEALTHREG" }, + + -- Defense + { p = "Increased Defense %+(%d+)", s = "DEFENSE" }, + { p = "Defense %+(%d+)", s = "DEFENSE" }, + { p = "%+(%d+) Defense", s = "DEFENSE" }, + { p = "防御技能提高(%d+)", s = "DEFENSE" }, + { p = "防御等级提高(%d+)", s = "DEFENSE" }, + + -- Avoidance + { p = "dodge.-by (%d+)%%", s = "DODGE" }, + { p = "躲闪.-(%d+)%%", s = "DODGE" }, + { p = "parry.-by (%d+)%%", s = "PARRY" }, + { p = "招架.-(%d+)%%", s = "PARRY" }, + { p = "block attacks.-by (%d+)%%", s = "BLOCK" }, + { p = "格挡几率.-(%d+)%%", s = "BLOCK" }, + { p = "格挡率.-(%d+)%%", s = "BLOCK" }, + { p = "block value.-by (%d+)", s = "BLOCKVALUE" }, + { p = "格挡值.-(%d+)", s = "BLOCKVALUE" }, + + -- HP/Mana + { p = "%+(%d+) Health", s = "HEALTH" }, + { p = "%+(%d+) Hit Points", s = "HEALTH" }, + { p = "%+(%d+) 生命值", s = "HEALTH" }, + { p = "%+(%d+) Mana", s = "MANA" }, + { p = "%+(%d+) 法力值", s = "MANA" }, +} + +-------------------------------------------------------------------------------- +-- Debug helper: /gsdebug to toggle +-------------------------------------------------------------------------------- +local GS_DEBUG = false +local function GSDebug(msg) + if GS_DEBUG and DEFAULT_CHAT_FRAME then + DEFAULT_CHAT_FRAME:AddMessage("|cffFFFF00[GS]|r " .. tostring(msg)) + end +end + +SLASH_GSDEBUG1 = "/gsdebug" +SlashCmdList["GSDEBUG"] = function() + GS_DEBUG = not GS_DEBUG + DEFAULT_CHAT_FRAME:AddMessage("|cffFFFF00[GearScore]|r Debug: " .. (GS_DEBUG and "ON" or "OFF")) +end + +-------------------------------------------------------------------------------- +-- Stat parser: scan a VISIBLE tooltip's text lines directly +-- This is the most reliable method — no hidden tooltip, no library needed +-------------------------------------------------------------------------------- + +local function ScanTooltipForStats(tooltip) + if not tooltip then return nil end + local tipName = tooltip:GetName() + if not tipName then return nil end + + local numLines = tooltip:NumLines() + if not numLines or numLines < 2 then return nil end + + local stats = {} + for i = 2, numLines do + local lineObj = getglobal(tipName .. "TextLeft" .. i) + if lineObj then + local text = lineObj:GetText() + if text and text ~= "" then + for _, pat in ipairs(STAT_PATTERNS) do + local _, _, val = string.find(text, pat.p) + if val then + local n = tonumber(val) + if n and n > 0 then + stats[pat.s] = (stats[pat.s] or 0) + n + if pat.a then + stats[pat.a] = (stats[pat.a] or 0) + n + end + GSDebug(" L" .. i .. ": " .. pat.s .. "=" .. n .. " (" .. text .. ")") + end + break + end + end + end + end + end + + local hasAny = false + for _ in pairs(stats) do hasAny = true; break end + return hasAny and stats or nil +end + +local function ParseItemWithLib(link) + if not link then return nil end + if not AceLibrary or not AceLibrary.HasInstance then return nil end + if not AceLibrary:HasInstance("ItemBonusLib-1.0") then return nil end + local lib = AceLibrary("ItemBonusLib-1.0") + if not lib or not lib.ScanItem then return nil end + local ok, result = pcall(function() return lib:ScanItem(link, true) end) + if ok and result then + local hasAny = false + for _ in pairs(result) do hasAny = true; break end + if hasAny then return result end + end + return nil +end + +-------------------------------------------------------------------------------- +-- Compatibility multipliers +-------------------------------------------------------------------------------- + +local function GetArmorCompat(classToken, itemClass, itemSubClass, level) + if not itemClass or not ARMOR_CLASS_NAMES[itemClass] then return 1.0 end + if not itemSubClass or itemSubClass == "" then return 1.0 end + if ARMOR_MISC_NAMES[itemSubClass] then return 1.0 end + if SHIELD_NAMES[itemSubClass] then return 1.0 end + + local normalizedSub = SUBCLASS_NORMALIZE[itemSubClass] + if not normalizedSub then return 1.0 end + + local tbl = (level < 40) and ARMOR_COMPAT_LOW or ARMOR_COMPAT + local classTbl = tbl[classToken] + if not classTbl then return 1.0 end + return classTbl[normalizedSub] or 0.3 +end + +local function GetSlotCompat(classToken, specIdx, equipLoc) + if not equipLoc or equipLoc == "" then return 1.0 end + local key = classToken .. "_" .. specIdx + local tbl = SLOT_COMPAT[key] + if not tbl then return 1.0 end + local val = tbl[equipLoc] + if val ~= nil then return val end + return 1.0 +end + +-------------------------------------------------------------------------------- +-- Core scoring: 1-10 normalized scale +-- Score = (actual_EP / ideal_EP) * 10 * equipment_compat +-- ideal_EP = total_budget * best_efficiency_for_spec +-------------------------------------------------------------------------------- + +local function GetBestEfficiency(weights) + local best = 0 + for stat, w in pairs(weights) do + local cost = BUDGET_COST[stat] + if cost and cost > 0 and w > 0 then + local eff = w / cost + if eff > best then best = eff end + end + end + return best +end + +local function CalcRawEP(bonuses, weights) + if not bonuses or not weights then return 0 end + local ep = 0 + for stat, value in pairs(bonuses) do + local w = weights[stat] + if w and w > 0 then + ep = ep + value * w + end + end + return ep +end + +local function CalcTotalBudget(bonuses) + if not bonuses then return 0 end + local total = 0 + for stat, value in pairs(bonuses) do + local cost = BUDGET_COST[stat] + if cost and cost > 0 then + total = total + math.abs(value) * cost + end + end + return total +end + +local function CalcNormalizedScore(bonuses, weights, armorCompat, slotCompat) + local rawEP = CalcRawEP(bonuses, weights) + local totalBudget = CalcTotalBudget(bonuses) + local bestEff = GetBestEfficiency(weights) + + if totalBudget <= 0 or bestEff <= 0 then return 0 end + + local idealEP = totalBudget * bestEff + local rawScore = (rawEP / idealEP) * 10 + + local compat = (armorCompat or 1.0) * (slotCompat or 1.0) + local finalScore = rawScore * compat + + if finalScore < 0 then finalScore = 0 end + if finalScore > 10 then finalScore = 10 end + + return math.floor(finalScore * 10 + 0.5) / 10 +end + +-------------------------------------------------------------------------------- +-- Score color (1-10 scale) +-------------------------------------------------------------------------------- + +local function ScoreColorHex(score) + if score >= 9 then return "ffFF8800" end + if score >= 7 then return "ff00FF00" end + if score >= 5 then return "ffFFFFFF" end + if score >= 3 then return "ffFFFF00" end + return "ffFF4444" +end + +local function ScoreLabel(score) + if score >= 9 then return "极品" end + if score >= 7 then return "优秀" end + if score >= 5 then return "适用" end + if score >= 3 then return "一般" end + return "不适" +end + +-------------------------------------------------------------------------------- +-- Main tooltip function +-------------------------------------------------------------------------------- + +function GS:AddScoreToTooltip(tooltip, link) + if not tooltip or not link then return end + if SFramesDB and SFramesDB.gearScore == false then return end + if tooltip._gsScoreAdded then return end + + local classToken = GetPlayerClassToken() + if not classToken then return end + local classData = WEIGHTS[classToken] + if not classData then return end + + GSDebug("Processing: " .. tostring(link)) + + -- Step 0: Check item quality — skip gray, penalize white + local quality = GetQualityFromLink(link) + if quality < 0 then + quality = GetQualityFromTooltip(tooltip) + end + local qualityMult = QUALITY_MULT[quality] or 0.75 + if quality == 0 then + GSDebug("Skipped: poor quality (gray) item") + return + end + GSDebug("Quality=" .. quality .. " mult=" .. qualityMult) + + -- Step 1: Try GetItemInfo with extracted item string (more reliable than full link) + local equipLoc, itemClass, itemSubClass + pcall(function() + local itemStr = ExtractItemString(link) + local infoArg = itemStr or link + GSDebug("GetItemInfo arg: " .. tostring(infoArg)) + local results = { GetItemInfo(infoArg) } + if results[1] then + itemClass = results[6] + itemSubClass = results[7] + for idx = 8, 10 do + local v = results[idx] + if type(v) == "string" and (v == "" or string.find(v, "^INVTYPE")) then + equipLoc = v + break + end + end + end + GSDebug("GetItemInfo: equip=" .. tostring(equipLoc) + .. " class=" .. tostring(itemClass) .. " sub=" .. tostring(itemSubClass)) + end) + + -- Step 2: If GetItemInfo failed, parse equip slot from tooltip text + if not equipLoc then + local ttEquip, ttArmor, ttClass = ParseEquipLocFromTooltip(tooltip) + if ttEquip then + equipLoc = ttEquip + if ttArmor and not itemSubClass then itemSubClass = ttArmor end + if ttClass and not itemClass then itemClass = ttClass end + GSDebug("Parsed from tooltip: equip=" .. tostring(equipLoc) .. " armor=" .. tostring(ttArmor)) + end + end + + -- Skip bags, ammo, tabards + if equipLoc == "INVTYPE_BAG" or equipLoc == "INVTYPE_AMMO" or equipLoc == "INVTYPE_TABARD" then + GSDebug("Skipped: bag/ammo/tabard") + return + end + + -- Skip non-equippable items (only if we got valid info) + if equipLoc and equipLoc ~= "" and not GS_EQUIP_LOCS[equipLoc] then + GSDebug("Skipped: not equippable (" .. tostring(equipLoc) .. ")") + return + end + + -- Step 3: Parse stats - try library first, then scan visible tooltip text + local bonuses = ParseItemWithLib(link) + if bonuses then + GSDebug("Stats from ItemBonusLib") + else + bonuses = ScanTooltipForStats(tooltip) + if bonuses then + GSDebug("Stats from tooltip text") + end + end + + if not bonuses then + GSDebug("No stats found, skipping") + return + end + + -- Step 4: No equipLoc = not equipment (potion, food, etc.) → skip + if not equipLoc or equipLoc == "" then + GSDebug("No equipLoc, not equipment, skipping") + return + end + + local level = GetPlayerLevel() + local primaryTab = GetPrimaryTabIndex() + local isHC = IsPlayerHardcore() + + local armorCompat = GetArmorCompat(classToken, itemClass, itemSubClass, level) + GSDebug("ArmorCompat=" .. armorCompat .. " class=" .. classToken) + + local specs = classData.specs + if not specs or table.getn(specs) == 0 then return end + + local scores = {} + local anyShow = false + for i, spec in ipairs(specs) do + local w = AdjustWeightsForLevel(spec.w, level, false) + local slotCompat = GetSlotCompat(classToken, i, equipLoc) + local s = CalcNormalizedScore(bonuses, w, armorCompat, slotCompat) + s = math.floor(s * qualityMult * 10 + 0.5) / 10 + if s < 1.0 and s > 0 then s = 1.0 end + table.insert(scores, { + name = spec.name, + color = spec.color, + score = s, + isPrimary = (spec.tab == primaryTab), + label = ScoreLabel(s), + }) + if s > 0 then anyShow = true end + end + + local hcScore = 0 + if classData.hc then + local hw = AdjustWeightsForLevel(classData.hc.w, level, true) + hcScore = CalcNormalizedScore(bonuses, hw, armorCompat, 1.0) + hcScore = math.floor(hcScore * qualityMult * 10 + 0.5) / 10 + if hcScore < 1.0 and hcScore > 0 then hcScore = 1.0 end + if hcScore > 0 then anyShow = true end + end + + if not anyShow then return end + + tooltip._gsScoreAdded = true + + tooltip:AddLine(" ") + tooltip:AddLine("|cffffd700── 装备评分 ──|r") + + for _, sd in ipairs(scores) do + local star = sd.isPrimary and "★ " or " " + local sStr = string.format("%.1f", sd.score) + local sColor = ScoreColorHex(sd.score) + local left = star .. "|c" .. sd.color .. sd.name .. "|r" + local right = "|c" .. sColor .. sStr .. " " .. sd.label .. "|r" + tooltip:AddDoubleLine(left, right, 1,1,1, 1,1,1) + end + + if classData.hc and hcScore > 0 then + local hcSColor = ScoreColorHex(hcScore) + local hcStar = isHC and "★ " or " " + local left = hcStar .. "|c" .. classData.hc.color .. "硬核|r" + local right = "|c" .. hcSColor .. string.format("%.1f", hcScore) + .. " " .. ScoreLabel(hcScore) .. "|r" + tooltip:AddDoubleLine(left, right, 1,1,1, 1,1,1) + end + + tooltip:Show() +end + +-------------------------------------------------------------------------------- +-- Self-contained tooltip hooks +-------------------------------------------------------------------------------- + +local function GS_TryEnhance(tooltip, link) + if not link then return end + pcall(function() GS:AddScoreToTooltip(tooltip, link) end) +end + +function GS:HookTooltips() + if self._hooked then return end + self._hooked = true + + local origHide = GameTooltip:GetScript("OnHide") + GameTooltip:SetScript("OnHide", function() + this._gsScoreAdded = nil + if origHide then origHide() end + end) + + local origBag = GameTooltip.SetBagItem + GameTooltip.SetBagItem = function(self, bag, slot) + self._gsScoreAdded = nil + local r1, r2, r3 = origBag(self, bag, slot) + GS_TryEnhance(self, GetContainerItemLink(bag, slot)) + return r1, r2, r3 + end + + local origInv = GameTooltip.SetInventoryItem + GameTooltip.SetInventoryItem = function(self, unit, slotId) + self._gsScoreAdded = nil + local r1, r2, r3 = origInv(self, unit, slotId) + if unit and slotId then + GS_TryEnhance(self, GetInventoryItemLink(unit, slotId)) + end + return r1, r2, r3 + end + + if GameTooltip.SetMerchantItem then + local orig = GameTooltip.SetMerchantItem + GameTooltip.SetMerchantItem = function(self, idx) + self._gsScoreAdded = nil + orig(self, idx) + if GetMerchantItemLink then GS_TryEnhance(self, GetMerchantItemLink(idx)) end + end + end + + if GameTooltip.SetQuestItem then + local orig = GameTooltip.SetQuestItem + GameTooltip.SetQuestItem = function(self, qtype, idx) + self._gsScoreAdded = nil + orig(self, qtype, idx) + if GetQuestItemLink then GS_TryEnhance(self, GetQuestItemLink(qtype, idx)) end + end + end + + if GameTooltip.SetQuestLogItem then + local orig = GameTooltip.SetQuestLogItem + GameTooltip.SetQuestLogItem = function(self, itype, idx) + self._gsScoreAdded = nil + orig(self, itype, idx) + if GetQuestLogItemLink then GS_TryEnhance(self, GetQuestLogItemLink(itype, idx)) end + end + end + + if GameTooltip.SetLootItem then + local orig = GameTooltip.SetLootItem + GameTooltip.SetLootItem = function(self, idx) + self._gsScoreAdded = nil + orig(self, idx) + if GetLootSlotLink then GS_TryEnhance(self, GetLootSlotLink(idx)) end + end + end + + if GameTooltip.SetLootRollItem then + local orig = GameTooltip.SetLootRollItem + GameTooltip.SetLootRollItem = function(self, rollId) + self._gsScoreAdded = nil + orig(self, rollId) + if GetLootRollItemLink then GS_TryEnhance(self, GetLootRollItemLink(rollId)) end + end + end + + if GameTooltip.SetAuctionItem then + local orig = GameTooltip.SetAuctionItem + GameTooltip.SetAuctionItem = function(self, atype, idx) + self._gsScoreAdded = nil + orig(self, atype, idx) + if GetAuctionItemLink then GS_TryEnhance(self, GetAuctionItemLink(atype, idx)) end + end + end + + if GameTooltip.SetTradeSkillItem then + local orig = GameTooltip.SetTradeSkillItem + GameTooltip.SetTradeSkillItem = function(self, skillIdx, reagentIdx) + self._gsScoreAdded = nil + orig(self, skillIdx, reagentIdx) + local link + if reagentIdx then + if GetTradeSkillReagentItemLink then link = GetTradeSkillReagentItemLink(skillIdx, reagentIdx) end + else + if GetTradeSkillItemLink then link = GetTradeSkillItemLink(skillIdx) end + end + GS_TryEnhance(self, link) + end + end + + if GameTooltip.SetCraftItem then + local orig = GameTooltip.SetCraftItem + GameTooltip.SetCraftItem = function(self, skill, slot) + self._gsScoreAdded = nil + orig(self, skill, slot) + if GetCraftReagentItemLink then GS_TryEnhance(self, GetCraftReagentItemLink(skill, slot)) end + end + end + + if GameTooltip.SetTradePlayerItem then + local orig = GameTooltip.SetTradePlayerItem + GameTooltip.SetTradePlayerItem = function(self, idx) + self._gsScoreAdded = nil + orig(self, idx) + if GetTradePlayerItemLink then GS_TryEnhance(self, GetTradePlayerItemLink(idx)) end + end + end + + if GameTooltip.SetTradeTargetItem then + local orig = GameTooltip.SetTradeTargetItem + GameTooltip.SetTradeTargetItem = function(self, idx) + self._gsScoreAdded = nil + orig(self, idx) + if GetTradeTargetItemLink then GS_TryEnhance(self, GetTradeTargetItemLink(idx)) end + end + end + + if GameTooltip.SetInboxItem then + local orig = GameTooltip.SetInboxItem + GameTooltip.SetInboxItem = function(self, mailID, attachIdx) + self._gsScoreAdded = nil + orig(self, mailID, attachIdx) + local left1 = getglobal("GameTooltipTextLeft1") + if left1 and GetItemLinkByName then + GS_TryEnhance(self, GetItemLinkByName(left1:GetText())) + end + end + end + + local origRef = SetItemRef + if origRef then + SetItemRef = function(link, text, button) + origRef(link, text, button) + if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then return end + pcall(function() + local _, _, itemStr = string.find(link or "", "(item:[%-?%d:]+)") + if itemStr then + ItemRefTooltip._gsScoreAdded = nil + GS:AddScoreToTooltip(ItemRefTooltip, itemStr) + end + end) + end + end +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- + +local initFrame = CreateFrame("Frame") +initFrame:RegisterEvent("PLAYER_ENTERING_WORLD") +initFrame:SetScript("OnEvent", function() + initFrame:UnregisterEvent("PLAYER_ENTERING_WORLD") + GS:HookTooltips() +end) diff --git a/IconMap.lua b/IconMap.lua new file mode 100644 index 0000000..ac6323c --- /dev/null +++ b/IconMap.lua @@ -0,0 +1,108 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI Icon Map (icon.tga) +-- +-- Texture: Interface\AddOns\Nanami-UI\img\icon +-- Size: 512x512 (8x8 grid, each cell 64x64) +-- Format: { left, right, top, bottom } tex coords +-- +-- Step = 1/8 = 0.125 per cell +-- +-- Layout (8 columns x 8 rows): +-- Row 1: logo | save | close | offline | quest | alliance | horde | character +-- Row 2: chat | settings | ai | backpack | mount | achieve | gold | friends +-- Row 3: exit | party | loot | dragon | profession | logout | worldmap | talent +-- Row 4: casting | attack | damage | latency | mail | questlog | spellbook | merchant +-- Row 5: star | potion | skull | heal | house | scroll | herb | key +-- Row 6: tank | auction | fishing | calendar | dungeon | lfg | charsheet | help +-- Row 7: sound | search | honor | menu | store | buff | ranged | speed +-- Row 8: energy | poison | armor | alchemy | cooking | camp | hearthstone| mining +-------------------------------------------------------------------------------- + +SFrames.ICON_PATH = "Interface\\AddOns\\Nanami-UI\\img\\icon" + +SFrames.ICON_TCOORDS = { + -- Row 1 (top = 0, bottom = 0.125) + ["logo"] = { 0, 0.125, 0, 0.125 }, -- R1C1 + ["save"] = { 0.125, 0.25, 0, 0.125 }, -- R1C2 + ["close"] = { 0.25, 0.375, 0, 0.125 }, -- R1C3 + ["offline"] = { 0.375, 0.5, 0, 0.125 }, -- R1C4 + ["quest"] = { 0.5, 0.625, 0, 0.125 }, -- R1C5 + ["alliance"] = { 0.625, 0.75, 0, 0.125 }, -- R1C6 + ["horde"] = { 0.75, 0.875, 0, 0.125 }, -- R1C7 + ["character"] = { 0.875, 1.0, 0, 0.125 }, -- R1C8 + + -- Row 2 (top = 0.125, bottom = 0.25) + ["chat"] = { 0, 0.125, 0.125, 0.25 }, -- R2C1 + ["settings"] = { 0.125, 0.25, 0.125, 0.25 }, -- R2C2 + ["ai"] = { 0.25, 0.375, 0.125, 0.25 }, -- R2C3 + ["backpack"] = { 0.375, 0.5, 0.125, 0.25 }, -- R2C4 + ["mount"] = { 0.5, 0.625, 0.125, 0.25 }, -- R2C5 + ["achieve"] = { 0.625, 0.75, 0.125, 0.25 }, -- R2C6 + ["gold"] = { 0.75, 0.875, 0.125, 0.25 }, -- R2C7 + ["friends"] = { 0.875, 1.0, 0.125, 0.25 }, -- R2C8 + + -- Row 3 (top = 0.25, bottom = 0.375) + ["exit"] = { 0, 0.125, 0.25, 0.375 }, -- R3C1 + ["party"] = { 0.125, 0.25, 0.25, 0.375 }, -- R3C2 + ["loot"] = { 0.25, 0.375, 0.25, 0.375 }, -- R3C3 + ["dragon"] = { 0.375, 0.5, 0.25, 0.375 }, -- R3C4 + ["profession"] = { 0.5, 0.625, 0.25, 0.375 }, -- R3C5 + ["logout"] = { 0.625, 0.75, 0.25, 0.375 }, -- R3C6 + ["worldmap"] = { 0.75, 0.875, 0.25, 0.375 }, -- R3C7 + ["talent"] = { 0.875, 1.0, 0.25, 0.375 }, -- R3C8 + + -- Row 4 (top = 0.375, bottom = 0.5) + ["casting"] = { 0, 0.125, 0.375, 0.5 }, -- R4C1 + ["attack"] = { 0.125, 0.25, 0.375, 0.5 }, -- R4C2 + ["damage"] = { 0.25, 0.375, 0.375, 0.5 }, -- R4C3 + ["latency"] = { 0.375, 0.5, 0.375, 0.5 }, -- R4C4 + ["mail"] = { 0.5, 0.625, 0.375, 0.5 }, -- R4C5 + ["questlog"] = { 0.625, 0.75, 0.375, 0.5 }, -- R4C6 + ["spellbook"] = { 0.75, 0.875, 0.375, 0.5 }, -- R4C7 + ["merchant"] = { 0.875, 1.0, 0.375, 0.5 }, -- R4C8 + + -- Row 5 (top = 0.5, bottom = 0.625) + ["star"] = { 0, 0.125, 0.5, 0.625 }, -- R5C1 + ["potion"] = { 0.125, 0.25, 0.5, 0.625 }, -- R5C2 + ["skull"] = { 0.25, 0.375, 0.5, 0.625 }, -- R5C3 + ["heal"] = { 0.375, 0.5, 0.5, 0.625 }, -- R5C4 + ["house"] = { 0.5, 0.625, 0.5, 0.625 }, -- R5C5 + ["scroll"] = { 0.625, 0.75, 0.5, 0.625 }, -- R5C6 + ["herb"] = { 0.75, 0.875, 0.5, 0.625 }, -- R5C7 + ["key"] = { 0.875, 1.0, 0.5, 0.625 }, -- R5C8 + + -- Row 6 (top = 0.625, bottom = 0.75) + ["tank"] = { 0, 0.125, 0.625, 0.75 }, -- R6C1 + ["auction"] = { 0.125, 0.25, 0.625, 0.75 }, -- R6C2 + ["fishing"] = { 0.25, 0.375, 0.625, 0.75 }, -- R6C3 + ["calendar"] = { 0.375, 0.5, 0.625, 0.75 }, -- R6C4 + ["dungeon"] = { 0.5, 0.625, 0.625, 0.75 }, -- R6C5 + ["lfg"] = { 0.625, 0.75, 0.625, 0.75 }, -- R6C6 + ["charsheet"] = { 0.75, 0.875, 0.625, 0.75 }, -- R6C7 + ["help"] = { 0.875, 1.0, 0.625, 0.75 }, -- R6C8 + + -- Row 7 (top = 0.75, bottom = 0.875) + ["sound"] = { 0, 0.125, 0.75, 0.875 }, -- R7C1 + ["search"] = { 0.125, 0.25, 0.75, 0.875 }, -- R7C2 + ["honor"] = { 0.25, 0.375, 0.75, 0.875 }, -- R7C3 + ["menu"] = { 0.375, 0.5, 0.75, 0.875 }, -- R7C4 + ["store"] = { 0.5, 0.625, 0.75, 0.875 }, -- R7C5 + ["buff"] = { 0.625, 0.75, 0.75, 0.875 }, -- R7C6 + ["ranged"] = { 0.75, 0.875, 0.75, 0.875 }, -- R7C7 + ["speed"] = { 0.875, 1.0, 0.75, 0.875 }, -- R7C8 + + -- Row 8 (top = 0.875, bottom = 1.0) + ["energy"] = { 0, 0.125, 0.875, 1.0 }, -- R8C1 + ["poison"] = { 0.125, 0.25, 0.875, 1.0 }, -- R8C2 + ["armor"] = { 0.25, 0.375, 0.875, 1.0 }, -- R8C3 + ["alchemy"] = { 0.375, 0.5, 0.875, 1.0 }, -- R8C4 + ["cooking"] = { 0.5, 0.625, 0.875, 1.0 }, -- R8C5 + ["camp"] = { 0.625, 0.75, 0.875, 1.0 }, -- R8C6 + ["hearthstone"] = { 0.75, 0.875, 0.875, 1.0 }, -- R8C7 + ["mining"] = { 0.875, 1.0, 0.875, 1.0 }, -- R8C8 + + -- Legacy aliases + ["admin"] = { 0, 0.125, 0.125, 0.25 }, -- -> chat + ["bank"] = { 0.25, 0.375, 0.25, 0.375 }, -- -> loot + ["perf"] = { 0.25, 0.375, 0.375, 0.5 }, -- -> damage +} diff --git a/InspectPanel.lua b/InspectPanel.lua new file mode 100644 index 0000000..f18e327 --- /dev/null +++ b/InspectPanel.lua @@ -0,0 +1,1590 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Inspect Panel (InspectPanel.lua) +-- Equipment-focused inspect panel with modern rounded UI +-- Talent viewing delegates to native InspectFrame +-------------------------------------------------------------------------------- + +SFrames.InspectPanel = {} + +local IP = SFrames.InspectPanel +local ipanel, equipPage +local inspectUnit = nil + +-------------------------------------------------------------------------------- +-- Theme (matches CharacterPanel) +-------------------------------------------------------------------------------- +local T = SFrames.ActiveTheme + +local FRAME_W = 340 +local FRAME_H = 380 +local HEADER_H = 28 +local CONTENT_TOP = -(HEADER_H + 2) +local SIDE_PAD = 8 +local INNER_PAD = 4 +local SLOT_SIZE = 32 +local SLOT_GAP = 2 + +local QUALITY_COLORS = { + [0] = { 0.62, 0.62, 0.62 }, [1] = { 1, 1, 1 }, + [2] = { 0.12, 1, 0 }, [3] = { 0.0, 0.44, 0.87 }, + [4] = { 0.64, 0.21, 0.93 }, [5] = { 1, 0.5, 0 }, +} + +local SLOT_LABEL = { + HeadSlot = "头部", NeckSlot = "颈部", ShoulderSlot = "肩部", + BackSlot = "背部", ChestSlot = "胸部", ShirtSlot = "衬衣", + WristSlot = "手腕", HandsSlot = "手套", WaistSlot = "腰带", + LegsSlot = "腿部", FeetSlot = "脚部", + Finger0Slot = "戒指1", Finger1Slot = "戒指2", + Trinket0Slot = "饰品1", Trinket1Slot = "饰品2", + MainHandSlot = "主手", SecondaryHandSlot = "副手", + RangedSlot = "远程", AmmoSlot = "弹药", TabardSlot = "战袍", +} + +local EQUIP_SLOTS_LEFT = { + { id = 1, name = "HeadSlot" }, { id = 2, name = "NeckSlot" }, + { id = 3, name = "ShoulderSlot" }, { id = 15, name = "BackSlot" }, + { id = 5, name = "ChestSlot" }, { id = 4, name = "ShirtSlot" }, + { id = 19, name = "TabardSlot" }, { id = 9, name = "WristSlot" }, +} +local EQUIP_SLOTS_RIGHT = { + { id = 10, name = "HandsSlot" }, { id = 6, name = "WaistSlot" }, + { id = 7, name = "LegsSlot" }, { id = 8, name = "FeetSlot" }, + { id = 11, name = "Finger0Slot" }, { id = 12, name = "Finger1Slot" }, + { id = 13, name = "Trinket0Slot" }, { id = 14, name = "Trinket1Slot" }, +} +local EQUIP_SLOTS_BOTTOM = { + { id = 16, name = "MainHandSlot" }, { id = 17, name = "SecondaryHandSlot" }, + { id = 18, name = "RangedSlot" }, { id = 0, name = "AmmoSlot" }, +} + +local widgetId = 0 + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + local bg = bgColor or T.bg + local bd = borderColor or T.border + frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) + frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) +end + +local function SetPixelBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 } + }) + if bgColor then frame:SetBackdropColor(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1) end + if borderColor then frame:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1) end +end + +local function CreateShadow(parent, size) + local s = CreateFrame("Frame", nil, parent) + local sz = size or 4 + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz) + s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0)) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 } + }) + s:SetBackdropColor(0, 0, 0, 0.6) + s:SetBackdropBorderColor(0, 0, 0, 0.45) + return s +end + +local function MakeSep(parent, x1, y1, x2, y2) + local sep = parent:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + sep:SetHeight(1) + sep:SetPoint("TOPLEFT", parent, "TOPLEFT", x1, y1) + sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", x2, y1) + return sep +end + +local function MakeFS(parent, size, justifyH, color) + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), size or 11, "OUTLINE") + fs:SetJustifyH(justifyH or "LEFT") + local c = color or T.valueText + fs:SetTextColor(c[1], c[2], c[3]) + return fs +end + +local function NextName(p) + widgetId = widgetId + 1 + return "SFramesIP" .. (p or "") .. tostring(widgetId) +end + +local function GetItemQualityFromLink(link) + if not link then return nil end + local _, _, q = string.find(link, "|c(%x+)|H") + if not q then return nil end + local m = { + ["ff9d9d9d"] = 0, ["ffffffff"] = 1, ["ff1eff00"] = 2, + ["ff0070dd"] = 3, ["ffa335ee"] = 4, ["ffff8000"] = 5, + } + return m[q] +end + +local function GetInspectUnit() + return inspectUnit or "target" +end + +-------------------------------------------------------------------------------- +-- Scroll helper +-------------------------------------------------------------------------------- +local function CreateScrollFrame(parent, width, height) + local holder = CreateFrame("Frame", NextName("SH"), parent) + holder:SetWidth(width) + holder:SetHeight(height) + + local scroll = CreateFrame("ScrollFrame", NextName("SF"), holder) + scroll:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0) + scroll:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -10, 0) + + local child = CreateFrame("Frame", NextName("SC"), scroll) + child:SetWidth(width - 14) + child:SetHeight(1) + scroll:SetScrollChild(child) + + local slider = CreateFrame("Slider", NextName("SB"), holder) + slider:SetWidth(6) + slider:SetPoint("TOPRIGHT", holder, "TOPRIGHT", -1, -2) + slider:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -1, 2) + slider:SetOrientation("VERTICAL") + slider:SetMinMaxValues(0, 1) + slider:SetValue(0) + SetPixelBackdrop(slider, { 0.1, 0.1, 0.12, 0.4 }, { 0.15, 0.15, 0.18, 0.3 }) + + local thumb = slider:CreateTexture(nil, "OVERLAY") + thumb:SetTexture("Interface\\Buttons\\WHITE8X8") + thumb:SetVertexColor(0.4, 0.4, 0.48, 0.7) + thumb:SetWidth(6) + thumb:SetHeight(28) + slider:SetThumbTexture(thumb) + + slider:SetScript("OnValueChanged", function() + scroll:SetVerticalScroll(this:GetValue()) + end) + scroll:EnableMouseWheel(1) + scroll:SetScript("OnMouseWheel", function() + local cur = slider:GetValue() + local step = 28 + local _, mx = slider:GetMinMaxValues() + if arg1 > 0 then + slider:SetValue(math.max(cur - step, 0)) + else + slider:SetValue(math.min(cur + step, mx)) + end + end) + + holder.scroll = scroll + holder.child = child + holder.slider = slider + holder.SetContentHeight = function(self, h) + child:SetHeight(h) + local visH = scroll:GetHeight() + local maxS = math.max(h - visH, 0) + slider:SetMinMaxValues(0, maxS) + if maxS == 0 then slider:Hide() else slider:Show() end + slider:SetValue(math.min(slider:GetValue(), maxS)) + end + return holder +end + +-------------------------------------------------------------------------------- +-- Main Frame (no tabs, single equipment view) +-------------------------------------------------------------------------------- +local function CreateMainFrame() + if ipanel then return ipanel end + + local f = CreateFrame("Frame", "SFramesInspectPanel", UIParent) + f:SetWidth(FRAME_W) + f:SetHeight(FRAME_H) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + f:SetFrameStrata("HIGH") + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + f:SetClampedToScreen(true) + SetRoundBackdrop(f, T.bg, T.border) + f.shadow = CreateShadow(f, 4) + f:Hide() + f:SetScript("OnHide", function() + if not IP.switchingToNative then + if ClearInspectPlayer then ClearInspectPlayer() end + inspectUnit = nil + end + IP:HideSummary() + end) + + table.insert(UISpecialFrames, "SFramesInspectPanel") + + f.classIcon = SFrames:CreateClassIcon(f, 16) + f.classIcon.overlay:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD + 2, -6) + + f.titleText = MakeFS(f, 12, "LEFT", T.gold) + f.titleText:SetPoint("LEFT", f.classIcon.overlay, "RIGHT", 4, 0) + + f.subtitleText = MakeFS(f, 9, "LEFT", T.dimText) + f.subtitleText:SetPoint("LEFT", f.titleText, "RIGHT", 6, 0) + + local function MakeHeaderBtn(parent, label, tooltip, offsetX, onClick) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(46) + btn:SetHeight(16) + btn:SetPoint("TOPRIGHT", parent, "TOPRIGHT", offsetX, -6) + btn:SetFrameLevel(parent:GetFrameLevel() + 3) + SetRoundBackdrop(btn, T.btnBg, T.btnBorder) + local txt = MakeFS(btn, 9, "CENTER", T.dimText) + txt:SetPoint("CENTER", btn, "CENTER", 0, 0) + txt:SetText(label) + btn:SetScript("OnClick", onClick) + btn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHover[1], T.btnHover[2], T.btnHover[3], T.btnHover[4]) + GameTooltip:SetOwner(this, "ANCHOR_BOTTOM") + GameTooltip:AddLine(tooltip, 1, 1, 1) + GameTooltip:Show() + end) + btn:SetScript("OnLeave", function() + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + GameTooltip:Hide() + end) + return btn + end + + MakeHeaderBtn(f, "荣誉", "打开原生荣誉面板", -77, function() IP:OpenNativeTab("honor") end) + MakeHeaderBtn(f, "天赋", "打开原生天赋面板", -27, function() IP:OpenNativeTab("talent") end) + + local closeBtn = CreateFrame("Button", nil, f) + closeBtn:SetWidth(16) + closeBtn:SetHeight(16) + closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -7, -6) + closeBtn:SetFrameLevel(f:GetFrameLevel() + 3) + SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder) + local closeTxt = MakeFS(closeBtn, 9, "CENTER", T.title) + closeTxt:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) + closeTxt:SetText("x") + closeBtn:SetScript("OnClick", function() f:Hide() end) + closeBtn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + end) + closeBtn:SetScript("OnLeave", function() + this:SetBackdropColor(T.buttonDownBg[1], T.buttonDownBg[2], T.buttonDownBg[3], T.buttonDownBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + end) + + MakeSep(f, 6, -HEADER_H, -6, -HEADER_H) + + equipPage = CreateFrame("Frame", NextName("Page"), f) + equipPage:SetPoint("TOPLEFT", f, "TOPLEFT", 4, CONTENT_TOP) + equipPage:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 4) + + ipanel = f + return f +end + +-------------------------------------------------------------------------------- +-- Title +-------------------------------------------------------------------------------- +function IP:UpdateTitle() + if not ipanel then return end + local unit = GetInspectUnit() + local name = UnitName(unit) or "未知" + local level = UnitLevel(unit) or "?" + local classLocal, classEn = UnitClass(unit) + classLocal = classLocal or "" + classEn = classEn or "" + local raceLocal = UnitRace(unit) or "" + + local cc = SFrames.Config and SFrames.Config.colors and SFrames.Config.colors.class and SFrames.Config.colors.class[classEn] + + if equipPage and equipPage.modelNameText then + if cc then + equipPage.modelNameText:SetTextColor(cc.r, cc.g, cc.b) + else + equipPage.modelNameText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + end + equipPage.modelNameText:SetText(name) + end + + if equipPage and equipPage.modelGuildText then + local guildName = GetGuildInfo and GetGuildInfo(unit) or nil + if guildName and guildName ~= "" then + equipPage.modelGuildText:SetText("<" .. guildName .. ">") + else + equipPage.modelGuildText:SetText("") + end + end + + local parts = {} + table.insert(parts, "Lv." .. tostring(level)) + table.insert(parts, raceLocal .. classLocal) + + if equipPage and equipPage.avgIlvl then + table.insert(parts, string.format("iLvl:%.1f", equipPage.avgIlvl)) + end + + if cc then + ipanel.titleText:SetTextColor(cc.r, cc.g, cc.b) + else + ipanel.titleText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + end + ipanel.titleText:SetText(table.concat(parts, " ")) + ipanel.subtitleText:SetText("") + + if ipanel.classIcon then + SFrames:SetClassIcon(ipanel.classIcon, classEn) + end +end + +-------------------------------------------------------------------------------- +-- Equipment Slot creation +-------------------------------------------------------------------------------- +function IP:CreateEquipSlot(parent, slotID, slotName) + local frame = CreateFrame("Button", NextName("Slot"), parent) + frame:SetWidth(SLOT_SIZE) + frame:SetHeight(SLOT_SIZE) + SetRoundBackdrop(frame, T.slotBg, T.slotBorder) + + local icon = frame:CreateTexture(nil, "ARTWORK") + icon:SetPoint("TOPLEFT", frame, "TOPLEFT", 4, -4) + icon:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -4, 4) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + frame.icon = icon + + local ilvlText = frame:CreateFontString(nil, "OVERLAY") + ilvlText:SetFont(GetFont(), 8, "OUTLINE") + ilvlText:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -2, 2) + ilvlText:SetTextColor(1, 0.82, 0) + ilvlText:SetText("") + frame.ilvlText = ilvlText + + local glow = frame:CreateTexture(nil, "OVERLAY") + glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + glow:SetBlendMode("ADD") + glow:SetAlpha(0.8) + glow:SetWidth(SLOT_SIZE * 2) + glow:SetHeight(SLOT_SIZE * 2) + glow:SetPoint("CENTER", frame, "CENTER", 0, 0) + glow:Hide() + frame.qualGlow = glow + + frame.slotID = slotID + frame.slotName = slotName + local _, emptyTex = GetInventorySlotInfo(slotName) + frame.emptyTexture = emptyTex + + frame:SetScript("OnEnter", function() + this:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4]) + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + local unit = GetInspectUnit() + if GetInventoryItemLink(unit, this.slotID) then + GameTooltip:SetInventoryItem(unit, this.slotID) + else + local label = SLOT_LABEL[this.slotName] or this.slotName + GameTooltip:AddLine(label .. " - 未装备", 0.5, 0.5, 0.5) + end + GameTooltip:Show() + end) + + frame:SetScript("OnLeave", function() + this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + GameTooltip:Hide() + end) + + frame:SetScript("OnClick", function() + if IsShiftKeyDown() then + local unit = GetInspectUnit() + local link = GetInventoryItemLink(unit, this.slotID) + if link and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then + ChatFrameEditBox:Insert(link) + elseif link then + if DEFAULT_CHAT_FRAME then + DEFAULT_CHAT_FRAME:AddMessage(link) + end + end + end + end) + + return frame +end + +-------------------------------------------------------------------------------- +-- Equipment Page +-------------------------------------------------------------------------------- +function IP:BuildEquipmentPage() + if not equipPage or equipPage.built then return end + equipPage.built = true + + local CONTENT_W = FRAME_W - INNER_PAD * 2 + local SCROLL_W = CONTENT_W - 10 + local contentH = FRAME_H - HEADER_H - 2 - INNER_PAD - 4 + local scrollArea = CreateScrollFrame(equipPage, CONTENT_W, contentH) + scrollArea:SetPoint("TOPLEFT", equipPage, "TOPLEFT", 0, 0) + equipPage.scrollArea = scrollArea + + local child = scrollArea.child + local cw = SCROLL_W + + local slotColW = SLOT_SIZE + 6 + local modelW = cw - slotColW * 2 - 26 + if modelW < 80 then modelW = 80 end + local modelH = 8 * (SLOT_SIZE + SLOT_GAP) - SLOT_GAP + + local modelBg = CreateFrame("Frame", nil, child) + modelBg:SetWidth(modelW) + modelBg:SetHeight(modelH) + modelBg:SetPoint("TOP", child, "TOP", 0, -4) + SetRoundBackdrop(modelBg, T.modelBg, T.modelBorder) + equipPage.modelBgFrame = modelBg + + local modelFrame = CreateFrame("Frame", nil, equipPage) + modelFrame:SetWidth(modelW - 8) + modelFrame:SetHeight(modelH - 24) + modelFrame:SetPoint("TOP", modelBg, "TOP", 0, -4) + modelFrame:SetFrameLevel(equipPage:GetFrameLevel() + 5) + + local model = CreateFrame("PlayerModel", NextName("Model"), modelFrame) + model:SetAllPoints(modelFrame) + equipPage.model = model + equipPage.modelFrame = modelFrame + + model:EnableMouse(true) + model:EnableMouseWheel(1) + model.rotating = false + model.panning = false + model.curFacing = 0.4 + model.curScale = 1.0 + model.posX = 0 + model.posY = 0 + + model:SetScript("OnMouseDown", function() + if arg1 == "LeftButton" then + this.rotating = true + this.startX = GetCursorPosition() + this.startFacing = this.curFacing or 0 + elseif arg1 == "RightButton" then + this.panning = true + local cx, cy = GetCursorPosition() + this.panStartX = cx + this.panStartY = cy + this.panOriginX = this.posX or 0 + this.panOriginY = this.posY or 0 + end + end) + model:SetScript("OnMouseUp", function() + if arg1 == "LeftButton" then + this.rotating = false + elseif arg1 == "RightButton" then + this.panning = false + end + end) + model:SetScript("OnMouseWheel", function() + local step = 0.1 + local ns = (this.curScale or 1) + arg1 * step + if ns < 0.2 then ns = 0.2 end + if ns > 4.0 then ns = 4.0 end + this.curScale = ns + this:SetModelScale(ns) + end) + model:SetScript("OnUpdate", function() + if this.rotating then + local cx = GetCursorPosition() + local diff = (cx - (this.startX or cx)) * 0.01 + local nf = (this.startFacing or 0) + diff + this.curFacing = nf + this:SetFacing(nf) + elseif this.panning then + local cx, cy = GetCursorPosition() + local es = this:GetEffectiveScale() + if es < 0.01 then es = 1 end + local dx = (cx - (this.panStartX or cx)) / (es * 35) + local dy = (cy - (this.panStartY or cy)) / (es * 35) + this.posX = (this.panOriginX or 0) + dx + this.posY = (this.panOriginY or 0) + dy + this:SetPosition(this.posY, 0, this.posX) + elseif this.autoRotateDir then + local speed = 1.5 * (arg1 or 0.016) + this.curFacing = (this.curFacing or 0) + speed * this.autoRotateDir + this:SetFacing(this.curFacing) + end + end) + model.ResetView = function(self) + self.curFacing = 0.4 + self.curScale = 1.0 + self.posX = 0 + self.posY = 0 + self:SetFacing(0.4) + self:SetModelScale(1.0) + self:SetPosition(0, 0, 0) + end + + -- Name/guild overlay floats on top of 3D model + local nameOverlay = CreateFrame("Frame", nil, equipPage) + nameOverlay:SetWidth(modelW) + nameOverlay:SetHeight(30) + nameOverlay:SetPoint("TOP", modelBg, "TOP", 0, -4) + nameOverlay:SetFrameLevel(equipPage:GetFrameLevel() + 7) + nameOverlay:EnableMouse(false) + + local modelNameText = nameOverlay:CreateFontString(nil, "OVERLAY") + modelNameText:SetFont(GetFont(), 11, "OUTLINE") + modelNameText:SetPoint("TOP", nameOverlay, "TOP", 0, -5) + modelNameText:SetJustifyH("CENTER") + modelNameText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + equipPage.modelNameText = modelNameText + + local modelGuildText = nameOverlay:CreateFontString(nil, "OVERLAY") + modelGuildText:SetFont(GetFont(), 9, "OUTLINE") + modelGuildText:SetPoint("TOP", modelNameText, "BOTTOM", 0, -1) + modelGuildText:SetJustifyH("CENTER") + modelGuildText:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + equipPage.modelGuildText = modelGuildText + + -- Model toolbar: unified strip with 3 segments (matches CharacterPanel) + local tbH = 14 + local segW = 24 + local tbW = segW * 3 + 2 + local toolbar = CreateFrame("Frame", nil, equipPage) + toolbar:SetWidth(tbW) + toolbar:SetHeight(tbH) + toolbar:SetPoint("BOTTOM", modelBg, "BOTTOM", 0, 5) + toolbar:SetFrameLevel(equipPage:GetFrameLevel() + 6) + SetPixelBackdrop(toolbar, { 0.04, 0.04, 0.06, 0.75 }, { 0.22, 0.22, 0.28, 0.5 }) + equipPage.modelToolbar = toolbar + + local function MakeToolBtn(parent, w, text, ox) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w) + btn:SetHeight(tbH - 2) + btn:SetPoint("LEFT", parent, "LEFT", ox, 0) + btn:SetFrameLevel(parent:GetFrameLevel() + 1) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 0, + }) + btn:SetBackdropColor(0, 0, 0, 0) + local fs = MakeFS(btn, 9, "CENTER", { 0.5, 0.5, 0.55 }) + fs:SetPoint("CENTER", btn, "CENTER", 0, 0) + fs:SetText(text) + btn.label = fs + btn:SetScript("OnEnter", function() + this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) + this.label:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + end) + btn:SetScript("OnLeave", function() + this:SetBackdropColor(0, 0, 0, 0) + this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + end) + return btn + end + + local rotLeft = MakeToolBtn(toolbar, segW, "<", 1) + rotLeft:SetScript("OnMouseDown", function() + model.autoRotateDir = 1 + this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4] or 0.6) + end) + rotLeft:SetScript("OnMouseUp", function() + model.autoRotateDir = nil + this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) + end) + rotLeft:SetScript("OnLeave", function() + this:SetBackdropColor(0, 0, 0, 0) + this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + model.autoRotateDir = nil + end) + equipPage.rotLeft = rotLeft + + local sep1 = toolbar:CreateTexture(nil, "OVERLAY") + sep1:SetTexture("Interface\\Buttons\\WHITE8X8") + sep1:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4] or 0.4) + sep1:SetWidth(1) + sep1:SetHeight(tbH - 4) + sep1:SetPoint("LEFT", toolbar, "LEFT", segW + 1, 0) + + local resetBtn = MakeToolBtn(toolbar, segW, "O", segW + 1) + resetBtn:SetScript("OnClick", function() model:ResetView() end) + resetBtn:SetScript("OnEnter", function() + this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) + this.label:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + GameTooltip:SetOwner(this, "ANCHOR_BOTTOM") + GameTooltip:AddLine("重置视角", 1, 1, 1) + GameTooltip:Show() + end) + resetBtn:SetScript("OnLeave", function() + this:SetBackdropColor(0, 0, 0, 0) + this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + GameTooltip:Hide() + end) + equipPage.resetBtn = resetBtn + + local sep2 = toolbar:CreateTexture(nil, "OVERLAY") + sep2:SetTexture("Interface\\Buttons\\WHITE8X8") + sep2:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4] or 0.4) + sep2:SetWidth(1) + sep2:SetHeight(tbH - 4) + sep2:SetPoint("LEFT", toolbar, "LEFT", segW * 2 + 1, 0) + + local rotRight = MakeToolBtn(toolbar, segW, ">", segW * 2 + 1) + rotRight:SetScript("OnMouseDown", function() + model.autoRotateDir = -1 + this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4] or 0.6) + end) + rotRight:SetScript("OnMouseUp", function() + model.autoRotateDir = nil + this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5) + end) + rotRight:SetScript("OnLeave", function() + this:SetBackdropColor(0, 0, 0, 0) + this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + model.autoRotateDir = nil + end) + equipPage.rotRight = rotRight + + local slotY = -6 + equipPage.equipSlots = {} + + for idx, si in ipairs(EQUIP_SLOTS_LEFT) do + local slot = self:CreateEquipSlot(child, si.id, si.name) + slot:SetPoint("TOPLEFT", child, "TOPLEFT", 2, slotY - (idx - 1) * (SLOT_SIZE + SLOT_GAP)) + equipPage.equipSlots[si.id] = slot + end + + for idx, si in ipairs(EQUIP_SLOTS_RIGHT) do + local slot = self:CreateEquipSlot(child, si.id, si.name) + slot:SetPoint("TOPRIGHT", child, "TOPRIGHT", -2, slotY - (idx - 1) * (SLOT_SIZE + SLOT_GAP)) + equipPage.equipSlots[si.id] = slot + end + + local bottomY = slotY - table.getn(EQUIP_SLOTS_LEFT) * (SLOT_SIZE + SLOT_GAP) - 4 + local bottomCount = table.getn(EQUIP_SLOTS_BOTTOM) + local totalBW = bottomCount * SLOT_SIZE + (bottomCount - 1) * (SLOT_GAP + 2) + local bsx = math.max((cw - totalBW) / 2, 4) + + for idx, si in ipairs(EQUIP_SLOTS_BOTTOM) do + local slot = self:CreateEquipSlot(child, si.id, si.name) + slot:SetPoint("TOPLEFT", child, "TOPLEFT", bsx + (idx - 1) * (SLOT_SIZE + SLOT_GAP + 1), bottomY) + equipPage.equipSlots[si.id] = slot + end + + local infoY = bottomY - SLOT_SIZE - 2 + equipPage.totalContentH = math.abs(infoY) + 8 + scrollArea:SetContentHeight(equipPage.totalContentH) +end + +local ALL_EQUIP_SLOTS = nil +local function GetAllEquipSlots() + if not ALL_EQUIP_SLOTS then + ALL_EQUIP_SLOTS = {} + for _, t in ipairs({ EQUIP_SLOTS_LEFT, EQUIP_SLOTS_RIGHT, EQUIP_SLOTS_BOTTOM }) do + for _, s in ipairs(t) do table.insert(ALL_EQUIP_SLOTS, s) end + end + end + return ALL_EQUIP_SLOTS +end + +function IP:UpdateEquipment() + if not equipPage or not equipPage.built then return end + local unit = GetInspectUnit() + + if equipPage.model then + equipPage.model:Show() + if equipPage.modelFrame then equipPage.modelFrame:Show() end + if equipPage.modelBgFrame then equipPage.modelBgFrame:Show() end + if equipPage.modelToolbar then equipPage.modelToolbar:Show() end + equipPage.model:SetUnit(unit) + equipPage.model:SetFacing(equipPage.model.curFacing or 0.4) + equipPage.model:SetModelScale(equipPage.model.curScale or 1.0) + equipPage.model:SetPosition(equipPage.model.posY or 0, 0, equipPage.model.posX or 0) + end + + local allSlots = GetAllEquipSlots() + local showIlvl = (not SFramesDB or SFramesDB.showItemLevel ~= false) and LibItem_Level + local totalIlvl, slotCount = 0, 0 + local scoreSlots = { [1]=true,[2]=true,[3]=true,[5]=true,[6]=true,[7]=true,[8]=true, + [9]=true,[10]=true,[11]=true,[12]=true,[13]=true,[14]=true,[15]=true,[16]=true,[17]=true,[18]=true } + + for _, si in ipairs(allSlots) do + local slot = equipPage.equipSlots[si.id] + if slot then + local tex = GetInventoryItemTexture(unit, si.id) + local link = GetInventoryItemLink(unit, si.id) + if tex then + slot.icon:SetTexture(tex) + slot.icon:SetVertexColor(1, 1, 1) + if slot.ilvlText then + if showIlvl and link then + local _, _, itemId = string.find(link, "item:(%d+)") + local ilvl = itemId and LibItem_Level[tonumber(itemId)] + slot.ilvlText:SetText(ilvl and tostring(ilvl) or "") + if ilvl and ilvl > 0 and scoreSlots[si.id] then + totalIlvl = totalIlvl + ilvl + slotCount = slotCount + 1 + end + else + slot.ilvlText:SetText("") + end + end + local quality = GetItemQualityFromLink(link) + if quality and quality >= 2 and QUALITY_COLORS[quality] then + local qc = QUALITY_COLORS[quality] + slot.qualGlow:SetVertexColor(qc[1], qc[2], qc[3]) + slot.qualGlow:Show() + else + slot.qualGlow:Hide() + end + else + if slot.emptyTexture then + slot.icon:SetTexture(slot.emptyTexture) + slot.icon:SetVertexColor(T.emptySlot[1], T.emptySlot[2], T.emptySlot[3], T.emptySlot[4]) + else slot.icon:SetTexture(nil) end + slot:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + slot.currentBorderColor = nil + slot.qualGlow:Hide() + if slot.ilvlText then slot.ilvlText:SetText("") end + end + end + end + + equipPage.avgIlvl = nil + if slotCount > 0 then + equipPage.avgIlvl = totalIlvl / slotCount + end + IP:UpdateTitle() +end + +-------------------------------------------------------------------------------- +-- Inspect Summary Side Panel (gear stats + equipment list) +-------------------------------------------------------------------------------- +local inspSummary +local SUMMARY_W = 220 +local SUMMARY_H = 380 +local SUMMARY_HEADER_H = 24 +local SUMMARY_ROW_H = 14 +local SUMMARY_SECTION_GAP = 4 + +local ENCHANTABLE_INSPECT_SLOTS = { + { id = 1, name = "HeadSlot", label = "头部" }, + { id = 3, name = "ShoulderSlot", label = "肩部" }, + { id = 15, name = "BackSlot", label = "背部" }, + { id = 5, name = "ChestSlot", label = "胸部" }, + { id = 9, name = "WristSlot", label = "手腕" }, + { id = 10, name = "HandsSlot", label = "手套" }, + { id = 7, name = "LegsSlot", label = "腿部" }, + { id = 8, name = "FeetSlot", label = "脚部" }, + { id = 11, name = "Finger0Slot", label = "戒指1" }, + { id = 12, name = "Finger1Slot", label = "戒指2" }, + { id = 16, name = "MainHandSlot", label = "主手" }, + { id = 17, name = "SecondaryHandSlot", label = "副手" }, + { id = 18, name = "RangedSlot", label = "远程" }, +} + +local inspScanTip + +local function EnsureInspTip() + if not inspScanTip then + inspScanTip = CreateFrame("GameTooltip", "SFramesIPScanTip", nil, "GameTooltipTemplate") + end + inspScanTip:SetOwner(UIParent, "ANCHOR_NONE") + return inspScanTip +end + +local STAT_PATS = { + { p = "%+(%d+)%s*力量", k = "str" }, + { p = "%+(%d+)%s*敏捷", k = "agi" }, + { p = "%+(%d+)%s*耐力", k = "sta" }, + { p = "%+(%d+)%s*智力", k = "int" }, + { p = "%+(%d+)%s*精神", k = "spi" }, + { p = "%+(%d+)%s*Strength", k = "str" }, + { p = "%+(%d+)%s*Agility", k = "agi" }, + { p = "%+(%d+)%s*Stamina", k = "sta" }, + { p = "%+(%d+)%s*Intellect", k = "int" }, + { p = "%+(%d+)%s*Spirit", k = "spi" }, + { p = "%+(%d+)%s*火焰抗性", k = "fireres" }, + { p = "%+(%d+)%s*自然抗性", k = "natureres" }, + { p = "%+(%d+)%s*冰霜抗性", k = "frostres" }, + { p = "%+(%d+)%s*暗影抗性", k = "shadowres" }, + { p = "%+(%d+)%s*奥术抗性", k = "arcaneres" }, + { p = "%+(%d+)%s*所有抗性", k = "allres" }, + { p = "%+(%d+)%s*Fire Resistance", k = "fireres" }, + { p = "%+(%d+)%s*Nature Resistance", k = "natureres" }, + { p = "%+(%d+)%s*Frost Resistance", k = "frostres" }, + { p = "%+(%d+)%s*Shadow Resistance", k = "shadowres" }, + { p = "%+(%d+)%s*Arcane Resistance", k = "arcaneres" }, + { p = "%+(%d+)%s*All Resistances", k = "allres" }, + { p = "%+(%d+)%s*攻击强度", k = "ap" }, + { p = "%+(%d+)%s*Attack Power", k = "ap" }, +} + +local EQUIP_PATS = { + { p = "伤害和治疗效果.-最多(%d+)点", k = "spelldmg" }, + { p = "法术造成的治疗效果.-最多(%d+)点", k = "healing" }, + { p = "击中几率提高(%d+)%%", k = "hit" }, + { p = "暴击几率提高(%d+)%%", k = "crit" }, + { p = "法术暴击几率提高(%d+)%%", k = "spellcrit" }, + { p = "法术命中几率提高(%d+)%%", k = "spellhit" }, + { p = "每5秒恢复(%d+)点法力", k = "mp5" }, + { p = "防御技能提高(%d+)", k = "defense" }, + { p = "躲闪几率提高(%d+)%%", k = "dodge" }, + { p = "招架几率提高(%d+)%%", k = "parry" }, + { p = "格挡几率提高(%d+)%%", k = "block" }, + { p = "damage and healing done by magical spells.-by up to (%d+)", k = "spelldmg" }, + { p = "healing done by spells.-by up to (%d+)", k = "healing" }, + { p = "chance to hit by (%d+)%%", k = "hit" }, + { p = "critical strike by (%d+)%%", k = "crit" }, + { p = "spell critical chance by (%d+)%%", k = "spellcrit" }, + { p = "Restores (%d+) mana per 5 sec", k = "mp5" }, + { p = "defense skill by (%d+)", k = "defense" }, + { p = "chance to dodge.-by (%d+)%%", k = "dodge" }, + { p = "chance to parry.-by (%d+)%%", k = "parry" }, + { p = "chance to block.-by (%d+)%%", k = "block" }, +} + +local PROC_INSP_ENCHANTS = { + "十字军", "Crusader", "吸取生命", "Lifestealing", + "灼热武器", "Fiery Weapon", "火焰武器", + "寒冰", "Icy Chill", "邪恶武器", "Unholy Weapon", + "恶魔杀手", "Demonslaying", "无法被缴械", "Cannot be Disarmed", +} + +local function IsInspEnchantLine(txt) + if string.find(txt, "%+%d") then return true end + if string.find(txt, "%d+%%") then return true end + for i = 1, table.getn(PROC_INSP_ENCHANTS) do + if string.find(txt, PROC_INSP_ENCHANTS[i]) then return true end + end + return false +end + +local function ScanInspectGearStats() + local unit = GetInspectUnit() + local stats = {} + local allSlots = GetAllEquipSlots() + local tip = EnsureInspTip() + + for _, si in ipairs(allSlots) do + local link = GetInventoryItemLink(unit, si.id) + if link then + tip:ClearLines() + tip:SetInventoryItem(unit, si.id) + local n = tip:NumLines() + if n and n > 0 then + for li = 1, n do + local obj = _G["SFramesIPScanTipTextLeft" .. li] + if obj then + local txt = obj:GetText() + if txt and txt ~= "" then + for _, sp in ipairs(STAT_PATS) do + local _, _, val = string.find(txt, sp.p) + if val then + stats[sp.k] = (stats[sp.k] or 0) + tonumber(val) + end + end + local r, g, b = obj:GetTextColor() + if g > 0.8 and r < 0.5 and b < 0.5 then + for _, ep in ipairs(EQUIP_PATS) do + local _, _, val = string.find(txt, ep.p) + if val then + stats[ep.k] = (stats[ep.k] or 0) + tonumber(val) + end + end + end + end + end + end + end + end + end + + if stats.allres and stats.allres > 0 then + local ar = stats.allres + stats.fireres = (stats.fireres or 0) + ar + stats.natureres = (stats.natureres or 0) + ar + stats.frostres = (stats.frostres or 0) + ar + stats.shadowres = (stats.shadowres or 0) + ar + stats.arcaneres = (stats.arcaneres or 0) + ar + stats.allres = nil + end + + return stats +end + +local function HasAnyStats(stats) + if not stats then return false end + for k, v in pairs(stats) do + if v and v > 0 then return true end + end + return false +end + +local function GetInspectEnchant(slotId) + local unit = GetInspectUnit() + local tip = EnsureInspTip() + tip:ClearLines() + tip:SetInventoryItem(unit, slotId) + local n = tip:NumLines() + if not n or n < 2 then return false, nil end + + for i = 2, n do + local obj = _G["SFramesIPScanTipTextLeft" .. i] + if obj then + local txt = obj:GetText() + if txt and txt ~= "" then + local r, g, b = obj:GetTextColor() + if g > 0.8 and r < 0.5 and b < 0.5 then + local skip = false + if string.find(txt, "装备:") or string.find(txt, "装备:") or string.find(txt, "Equip:") then + skip = true + elseif string.find(txt, "使用:") or string.find(txt, "使用:") or string.find(txt, "Use:") then + skip = true + elseif string.find(txt, "击中时可能") or string.find(txt, "Chance on hit") then + skip = true + elseif string.find(txt, "^%(") then + skip = true + elseif string.find(txt, "套装:") or string.find(txt, "套装:") or string.find(txt, "Set:") then + skip = true + end + if not skip and IsInspEnchantLine(txt) then + return true, txt + end + end + end + end + end + return false, nil +end + +-- Row helpers for summary panel +local function HideSummaryRows(parent) + if parent._r then + for i = 1, table.getn(parent._r) do + local r = parent._r[i] + if r and r.Hide then r:Hide() end + end + end + parent._r = {} +end + +local function AddSummaryHeader(p, txt, y, clr) + local f1 = MakeFS(p, 10, "LEFT", clr or T.sectionTitle) + f1:SetPoint("TOPLEFT", p, "TOPLEFT", 4, y) + f1:SetText(txt) + table.insert(p._r, f1) + + local s = p:CreateTexture(nil, "ARTWORK") + s:SetTexture("Interface\\Buttons\\WHITE8X8") + s:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + s:SetHeight(1) + s:SetPoint("TOPLEFT", p, "TOPLEFT", 4, y - 13) + s:SetPoint("TOPRIGHT", p, "TOPRIGHT", -4, y - 13) + table.insert(p._r, s) + return y - 16 +end + +local function AddSummaryRow(p, lbl, val, y, lc, vc) + local f1 = MakeFS(p, 9, "LEFT", lc or T.labelText) + f1:SetPoint("TOPLEFT", p, "TOPLEFT", 8, y) + f1:SetText(lbl) + table.insert(p._r, f1) + + local f2 = MakeFS(p, 9, "RIGHT", vc or T.valueText) + f2:SetPoint("TOPRIGHT", p, "TOPRIGHT", -12, y) + f2:SetWidth(80) + f2:SetJustifyH("RIGHT") + f2:SetText(val) + table.insert(p._r, f2) + return y - SUMMARY_ROW_H +end + +local INSP_STAT_COLORS = { + str = { 0.78, 0.61, 0.43 }, agi = { 0.52, 1, 0.52 }, + sta = { 0.75, 0.55, 0.25 }, int = { 0.41, 0.80, 0.94 }, + spi = { 1, 1, 1 }, +} +local INSP_RESIST_COLORS = { + arcaneres = { 0.95, 0.90, 0.40 }, fireres = { 1, 0.50, 0.15 }, + natureres = { 0.35, 0.90, 0.25 }, frostres = { 0.45, 0.70, 1.00 }, + shadowres = { 0.60, 0.35, 0.90 }, +} +local INSP_PHYS_COLOR = { 0.90, 0.75, 0.55 } +local INSP_SPELL_COLOR = { 0.60, 0.80, 1.00 } +local INSP_REGEN_COLOR = { 0.40, 0.90, 0.70 } +local INSP_DEF_COLOR = { 0.35, 0.90, 0.25 } +local INSP_ENCHANTED = { 0.30, 1, 0.30 } +local INSP_NO_ENCHANT = { 1, 0.35, 0.35 } + +local function BuildInspectSummary() + if inspSummary then return inspSummary end + + local f = CreateFrame("Frame", "SFramesInspectSummary", UIParent) + f:SetWidth(SUMMARY_W) + f:SetHeight(SUMMARY_H) + f:SetFrameStrata("HIGH") + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + f:SetClampedToScreen(true) + SetRoundBackdrop(f, T.bg, T.border) + CreateShadow(f, 4) + f:Hide() + + local hdr = MakeFS(f, 11, "LEFT", T.gold) + hdr:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -5) + hdr:SetText("装备属性") + + local tabW = 52 + f.tabs = {} + f.curTab = 1 + local tNames = { "属性", "装备" } + for i = 1, 2 do + local btn = CreateFrame("Button", NextName("IST"), f) + btn:SetWidth(tabW) + btn:SetHeight(16) + btn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -(8 + (2 - i) * (tabW + 2) + 18), -4) + btn:SetFrameLevel(f:GetFrameLevel() + 2) + SetPixelBackdrop(btn, T.btnBg, T.btnBorder) + local lbl = MakeFS(btn, 9, "CENTER", T.dimText) + lbl:SetPoint("CENTER", btn, "CENTER", 0, 0) + lbl:SetText(tNames[i]) + btn.lbl = lbl + btn.idx = i + btn:SetScript("OnClick", function() IP:SetSummaryTab(this.idx) end) + btn:SetScript("OnEnter", function() + if this.idx ~= (inspSummary.curTab or 1) then + this:SetBackdropColor(T.btnHover[1], T.btnHover[2], T.btnHover[3], T.btnHover[4]) + end + end) + btn:SetScript("OnLeave", function() + if this.idx ~= (inspSummary.curTab or 1) then + SetPixelBackdrop(this, T.btnBg, T.btnBorder) + end + end) + f.tabs[i] = btn + end + + local closeBtn = CreateFrame("Button", nil, f) + closeBtn:SetWidth(14) + closeBtn:SetHeight(14) + closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -5) + closeBtn:SetFrameLevel(f:GetFrameLevel() + 3) + SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder) + local closeTxt = MakeFS(closeBtn, 8, "CENTER", T.title) + closeTxt:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) + closeTxt:SetText("x") + closeBtn:SetScript("OnClick", function() f:Hide() end) + closeBtn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + end) + closeBtn:SetScript("OnLeave", function() + this:SetBackdropColor(T.buttonDownBg[1], T.buttonDownBg[2], T.buttonDownBg[3], T.buttonDownBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + end) + + MakeSep(f, 4, -SUMMARY_HEADER_H, -4, -SUMMARY_HEADER_H) + + local cH = SUMMARY_H - SUMMARY_HEADER_H - 8 + local cW = SUMMARY_W - 8 + + f.statsScroll = CreateScrollFrame(f, cW, cH) + f.statsScroll:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -(SUMMARY_HEADER_H + 2)) + + f.equipScroll = CreateScrollFrame(f, cW, cH) + f.equipScroll:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -(SUMMARY_HEADER_H + 2)) + f.equipScroll:Hide() + + inspSummary = f + return f +end + +function IP:SetSummaryTab(idx) + if not inspSummary then return end + inspSummary.curTab = idx + for i = 1, 2 do + local btn = inspSummary.tabs[i] + if i == idx then + SetPixelBackdrop(btn, T.tabActiveBg, T.tabActiveBorder) + btn.lbl:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + else + SetPixelBackdrop(btn, T.btnBg, T.btnBorder) + btn.lbl:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + end + end + if idx == 1 then + inspSummary.statsScroll:Show() + inspSummary.equipScroll:Hide() + local ok, err = pcall(function() IP:BuildInspectStats() end) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444IP BuildStats err: " .. tostring(err) .. "|r") + end + else + inspSummary.statsScroll:Hide() + inspSummary.equipScroll:Show() + local ok, err = pcall(function() IP:BuildInspectEquipList() end) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444IP BuildEquip err: " .. tostring(err) .. "|r") + end + end +end + +function IP:BuildInspectStats() + if not inspSummary then return end + local child = inspSummary.statsScroll.child + HideSummaryRows(child) + local y = -4 + + local stats = ScanInspectGearStats() + + local pNames = { + { k = "str", n = "力量" }, { k = "agi", n = "敏捷" }, + { k = "sta", n = "耐力" }, { k = "int", n = "智力" }, + { k = "spi", n = "精神" }, + } + local hasP = false + for _, p in ipairs(pNames) do + if stats[p.k] and stats[p.k] > 0 then hasP = true; break end + end + if hasP then + y = AddSummaryHeader(child, "装备主属性:", y) + for _, p in ipairs(pNames) do + local v = stats[p.k] or 0 + if v > 0 then + local c = INSP_STAT_COLORS[p.k] or T.valueText + y = AddSummaryRow(child, p.n, "+" .. tostring(v), y, c, c) + end + end + y = y - SUMMARY_SECTION_GAP + end + + local hasPhys = (stats.ap and stats.ap > 0) or (stats.hit and stats.hit > 0) or (stats.crit and stats.crit > 0) + if hasPhys then + y = AddSummaryHeader(child, "物理:", y, INSP_PHYS_COLOR) + if stats.ap and stats.ap > 0 then + y = AddSummaryRow(child, "攻击强度", "+" .. tostring(stats.ap), y, nil, INSP_PHYS_COLOR) + end + if stats.hit and stats.hit > 0 then + y = AddSummaryRow(child, "命中", "+" .. tostring(stats.hit) .. "%", y) + end + if stats.crit and stats.crit > 0 then + y = AddSummaryRow(child, "暴击", "+" .. tostring(stats.crit) .. "%", y) + end + y = y - SUMMARY_SECTION_GAP + end + + local hasSpell = (stats.spelldmg and stats.spelldmg > 0) or (stats.healing and stats.healing > 0) + or (stats.spellcrit and stats.spellcrit > 0) or (stats.spellhit and stats.spellhit > 0) + if hasSpell then + y = AddSummaryHeader(child, "法术:", y, INSP_SPELL_COLOR) + if stats.spelldmg and stats.spelldmg > 0 then + y = AddSummaryRow(child, "法术伤害", "+" .. tostring(stats.spelldmg), y, nil, INSP_SPELL_COLOR) + end + if stats.healing and stats.healing > 0 then + y = AddSummaryRow(child, "法术治疗", "+" .. tostring(stats.healing), y) + end + if stats.spellcrit and stats.spellcrit > 0 then + y = AddSummaryRow(child, "法术暴击", "+" .. tostring(stats.spellcrit) .. "%", y) + end + if stats.spellhit and stats.spellhit > 0 then + y = AddSummaryRow(child, "法术命中", "+" .. tostring(stats.spellhit) .. "%", y) + end + y = y - SUMMARY_SECTION_GAP + end + + local hasDef = (stats.defense and stats.defense > 0) or (stats.dodge and stats.dodge > 0) + or (stats.parry and stats.parry > 0) or (stats.block and stats.block > 0) + if hasDef then + y = AddSummaryHeader(child, "防御:", y, INSP_DEF_COLOR) + if stats.defense and stats.defense > 0 then + y = AddSummaryRow(child, "防御技能", "+" .. tostring(stats.defense), y, nil, INSP_DEF_COLOR) + end + if stats.dodge and stats.dodge > 0 then + y = AddSummaryRow(child, "躲闪", "+" .. tostring(stats.dodge) .. "%", y) + end + if stats.parry and stats.parry > 0 then + y = AddSummaryRow(child, "招架", "+" .. tostring(stats.parry) .. "%", y) + end + if stats.block and stats.block > 0 then + y = AddSummaryRow(child, "格挡", "+" .. tostring(stats.block) .. "%", y) + end + y = y - SUMMARY_SECTION_GAP + end + + if stats.mp5 and stats.mp5 > 0 then + y = AddSummaryHeader(child, "回复:", y, INSP_REGEN_COLOR) + y = AddSummaryRow(child, "装备回蓝", tostring(stats.mp5) .. " MP/5s", y, nil, INSP_REGEN_COLOR) + y = y - SUMMARY_SECTION_GAP + end + + local resInfo = { + { k = "arcaneres", n = "奥术抗性" }, { k = "fireres", n = "火焰抗性" }, + { k = "natureres", n = "自然抗性" }, { k = "frostres", n = "冰霜抗性" }, + { k = "shadowres", n = "暗影抗性" }, + } + local hasRes = false + for _, ri in ipairs(resInfo) do + if stats[ri.k] and stats[ri.k] > 0 then hasRes = true; break end + end + if hasRes then + y = AddSummaryHeader(child, "抗性:", y, INSP_RESIST_COLORS.arcaneres) + for _, ri in ipairs(resInfo) do + local v = stats[ri.k] or 0 + if v > 0 then + local c = INSP_RESIST_COLORS[ri.k] or T.valueText + y = AddSummaryRow(child, ri.n, "+" .. tostring(v), y, c, c) + end + end + y = y - SUMMARY_SECTION_GAP + end + + if not HasAnyStats(stats) then + y = AddSummaryHeader(child, "提示:", y, T.dimText) + local noData = MakeFS(child, 9, "LEFT", T.dimText) + noData:SetPoint("TOPLEFT", child, "TOPLEFT", 8, y) + noData:SetText("未获取到装备属性数据") + table.insert(child._r, noData) + y = y - SUMMARY_ROW_H + end + + inspSummary.statsScroll:SetContentHeight(math.abs(y) + 12) +end + +function IP:BuildInspectEquipList() + if not inspSummary then return end + local child = inspSummary.equipScroll.child + HideSummaryRows(child) + local y = -4 + + y = AddSummaryHeader(child, "装备列表 & 附魔检查:", y, T.gold) + + local unit = GetInspectUnit() + local totalSlots = 0 + local enchCount = 0 + + for si = 1, table.getn(ENCHANTABLE_INSPECT_SLOTS) do + local slot = ENCHANTABLE_INSPECT_SLOTS[si] + local link = GetInventoryItemLink(unit, slot.id) + + if link then + totalSlots = totalSlots + 1 + local _, _, rawName = string.find(link, "%[(.-)%]") + local itemName = rawName or slot.label + + local quality = nil + local _, _, qH = string.find(link, "|c(%x+)|H") + local qMap = { + ["ff9d9d9d"] = 0, ["ffffffff"] = 1, ["ff1eff00"] = 2, + ["ff0070dd"] = 3, ["ffa335ee"] = 4, ["ffff8000"] = 5, + } + if qH then quality = qMap[qH] end + local nc = (quality and QUALITY_COLORS[quality]) or T.valueText + + local sf = MakeFS(child, 8, "LEFT", T.dimText) + sf:SetPoint("TOPLEFT", child, "TOPLEFT", 4, y) + sf:SetText(slot.label) + sf:SetWidth(32) + table.insert(child._r, sf) + + local nf = MakeFS(child, 9, "LEFT", nc) + nf:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y) + nf:SetWidth(120) + if string.len(itemName) > 16 then + itemName = string.sub(itemName, 1, 14) .. ".." + end + nf:SetText(itemName) + table.insert(child._r, nf) + + local hasE, eTxt = false, nil + local eOk, eR1, eR2 = pcall(GetInspectEnchant, slot.id) + if eOk then hasE = eR1; eTxt = eR2 end + + local ico = MakeFS(child, 9, "RIGHT") + ico:SetPoint("TOPRIGHT", child, "TOPRIGHT", -8, y) + if hasE then + ico:SetTextColor(INSP_ENCHANTED[1], INSP_ENCHANTED[2], INSP_ENCHANTED[3]) + ico:SetText("*") + enchCount = enchCount + 1 + else + ico:SetTextColor(INSP_NO_ENCHANT[1], INSP_NO_ENCHANT[2], INSP_NO_ENCHANT[3]) + ico:SetText("-") + end + table.insert(child._r, ico) + + y = y - SUMMARY_ROW_H + + if hasE and eTxt then + local ef = MakeFS(child, 8, "LEFT", INSP_ENCHANTED) + ef:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y) + ef:SetWidth(145) + ef:SetText(" " .. eTxt) + table.insert(child._r, ef) + y = y - 12 + elseif not hasE then + local ef = MakeFS(child, 8, "LEFT", INSP_NO_ENCHANT) + ef:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y) + ef:SetText(" 未附魔") + table.insert(child._r, ef) + y = y - 12 + end + y = y - 2 + else + local sf = MakeFS(child, 8, "LEFT", T.dimText) + sf:SetPoint("TOPLEFT", child, "TOPLEFT", 4, y) + sf:SetText(slot.label) + table.insert(child._r, sf) + + local ef = MakeFS(child, 9, "LEFT", T.dimText) + ef:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y) + ef:SetText("-- 未装备 --") + table.insert(child._r, ef) + y = y - SUMMARY_ROW_H - 2 + end + end + + y = y - SUMMARY_SECTION_GAP + y = AddSummaryHeader(child, "附魔统计:", y, T.gold) + local sc2 = (enchCount == totalSlots and totalSlots > 0) and INSP_ENCHANTED or INSP_NO_ENCHANT + y = AddSummaryRow(child, "已附魔/总装备", enchCount .. "/" .. totalSlots, y, nil, sc2) + if enchCount < totalSlots then + y = AddSummaryRow(child, "缺少附魔", tostring(totalSlots - enchCount) .. " 件", y, INSP_NO_ENCHANT, INSP_NO_ENCHANT) + end + + inspSummary.equipScroll:SetContentHeight(math.abs(y) + 12) +end + +function IP:ShowSummary() + BuildInspectSummary() + if ipanel and ipanel:IsShown() then + inspSummary:ClearAllPoints() + inspSummary:SetPoint("TOPLEFT", ipanel, "TOPRIGHT", 2, 0) + else + inspSummary:ClearAllPoints() + inspSummary:SetPoint("CENTER", UIParent, "CENTER", 200, 0) + end + inspSummary:Show() + + IP:SetSummaryTab(2) +end + +function IP:HideSummary() + if inspSummary then inspSummary:Hide() end +end + +function IP:RefreshSummary() + if not inspSummary or not inspSummary:IsShown() then return end + IP:SetSummaryTab(inspSummary.curTab or 1) +end + +-------------------------------------------------------------------------------- +-- Open native InspectFrame for a specific tab (honor / talent) +-- Turtle WoW tabs: 1=角色, 2=荣誉, 3=天赋, ... +-------------------------------------------------------------------------------- +local NATIVE_TAB_INDEX = { honor = 2, talent = 3 } + +function IP:OpenNativeTab(which) + local unit = inspectUnit or "target" + if not UnitExists(unit) then return end + + local tabIdx = NATIVE_TAB_INDEX[which] + + self.switchingToNative = true + self.allowNativeFrame = true + + if ipanel then ipanel:Hide() end + self.switchingToNative = false + + if not IsAddOnLoaded("Blizzard_InspectUI") then + pcall(function() LoadAddOn("Blizzard_InspectUI") end) + end + + local inspF = _G["InspectFrame"] + if inspF and inspF._origShow then + inspF.Show = inspF._origShow + inspF._origShow = nil + end + + if IP._origInspectUnit then + IP._origInspectUnit(unit) + end + + inspF = _G["InspectFrame"] + if inspF then + inspF:SetAlpha(1) + inspF:Show() + end + + if tabIdx then + local tab = _G["InspectFrameTab" .. tabIdx] + if tab and tab.Click then + tab:Click() + elseif tab and tab:GetScript("OnClick") then + tab:GetScript("OnClick")() + end + end + + if not self._resetFrame then + self._resetFrame = CreateFrame("Frame", nil, UIParent) + end + self._resetFrame.elapsed = 0 + self._resetFrame:SetScript("OnUpdate", function() + this.elapsed = (this.elapsed or 0) + (arg1 or 0) + if this.elapsed > 1.0 then + IP.allowNativeFrame = false + this:SetScript("OnUpdate", nil) + end + end) +end + +-------------------------------------------------------------------------------- +-- Open / Close / Toggle +-------------------------------------------------------------------------------- +function IP:Open(unit) + inspectUnit = unit or "target" + + if not UnitExists(inspectUnit) then + if SFrames and SFrames.Print then + SFrames:Print("没有有效的观察目标。") + end + return + end + if not UnitIsPlayer(inspectUnit) then + if SFrames and SFrames.Print then + SFrames:Print("只能观察其他玩家。") + end + return + end + + CreateMainFrame() + self:BuildEquipmentPage() + + NotifyInspect(inspectUnit) + + ipanel:Show() + self:UpdateTitle() + self:UpdateEquipment() + self:ShowSummary() +end + +function IP:Close() + self:HideSummary() + if ipanel then ipanel:Hide() end + if ClearInspectPlayer then ClearInspectPlayer() end + inspectUnit = nil +end + +function IP:Toggle(unit) + if ipanel and ipanel:IsShown() then + self:Close() + else + self:Open(unit) + end +end + +-------------------------------------------------------------------------------- +-- Events & Hooking +-------------------------------------------------------------------------------- +local eventFrame = CreateFrame("Frame", "SFramesIPEvents", UIParent) +eventFrame:RegisterEvent("UNIT_INVENTORY_CHANGED") +eventFrame:RegisterEvent("ADDON_LOADED") + +eventFrame:SetScript("OnEvent", function() + if event == "ADDON_LOADED" then + -- Re-hook InspectUnit whenever a new addon loads (in case it replaces our hook) + if type(InspectUnit) == "function" and InspectUnit ~= IP._ourHook then + IP._origInspectUnit = InspectUnit + local hook = function(unit) + if (SFramesDB and SFramesDB.enableInspect == false) or IP.allowNativeFrame then + if IP._origInspectUnit then + IP._origInspectUnit(unit) + end + return + end + IP:Open(unit or "target") + end + IP._ourHook = hook + InspectUnit = hook + end + return + end + + if event == "UNIT_INVENTORY_CHANGED" then + if not ipanel or not ipanel:IsShown() then return end + local unit = GetInspectUnit() + if arg1 and UnitIsUnit(arg1, unit) then + IP:UpdateEquipment() + IP:RefreshSummary() + end + end +end) + +-- Hook UnitPopup_OnClick to catch right-click menu "Inspect" +if UnitPopup_OnClick then + local orig_UnitPopup_OnClick = UnitPopup_OnClick + UnitPopup_OnClick = function() + local button = this and this.value + if button == "INSPECT" and not (SFramesDB and SFramesDB.enableInspect == false) then + local dropdownFrame = _G[UIDROPDOWNMENU_INIT_MENU] + local unit = (dropdownFrame and dropdownFrame.unit) or "target" + IP:Open(unit) + return + end + return orig_UnitPopup_OnClick() + end +end + +-- Hook InspectUnit +local function HookInspectUnit() + if type(InspectUnit) ~= "function" then return end + if InspectUnit == IP._ourHook then return end + IP._origInspectUnit = InspectUnit + local hook = function(unit) + if (SFramesDB and SFramesDB.enableInspect == false) or IP.allowNativeFrame then + if IP._origInspectUnit then + IP._origInspectUnit(unit) + end + return + end + IP:Open(unit or "target") + end + IP._ourHook = hook + InspectUnit = hook +end + +-- Hook ShowUIPanel to block native InspectFrame when we don't want it +if ShowUIPanel then + local _origShowUIPanel = ShowUIPanel + ShowUIPanel = function(frame) + if frame and frame.GetName and not IP.allowNativeFrame and not (SFramesDB and SFramesDB.enableInspect == false) then + local name = frame:GetName() or "" + if string.find(name, "Inspect") then + return + end + end + return _origShowUIPanel(frame) + end +end + +HookInspectUnit() diff --git a/Mail.lua b/Mail.lua new file mode 100644 index 0000000..36db9a0 --- /dev/null +++ b/Mail.lua @@ -0,0 +1,1633 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Mail UI (Mail.lua) +-- Replaces MailFrame with Nanami-UI styled interface +-- Tabs: Inbox / Send +-- Inbox: select-all / per-item checkboxes, batch collect +-- Send: multi-item queue, split into individual sends +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.Mail = {} +local ML = SFrames.Mail +SFramesDB = SFramesDB or {} + +-- Save original Blizzard functions BEFORE other addons (TurtleMail etc.) hook them. +-- File-level locals are captured at load time, which is before later-alphabetical addons. +local _ClickSendMailItemButton = ClickSendMailItemButton +local _PickupContainerItem = PickupContainerItem +local _ClearCursor = ClearCursor +local _SendMail = SendMail + +-------------------------------------------------------------------------------- +-- Theme +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + moneyGold = { 1, 0.84, 0.0 }, + moneySilver = { 0.78, 0.78, 0.78 }, + moneyCopper = { 0.71, 0.43, 0.18 }, + codColor = { 1.0, 0.35, 0.35 }, + unreadMark = { 1.0, 0.85, 0.3 }, + successText = { 0.3, 1.0, 0.4 }, + errorText = { 1.0, 0.3, 0.3 }, + friendColor = { 0.3, 1.0, 0.5 }, + guildColor = { 0.4, 0.8, 1.0 }, + whoColor = { 0.85, 0.85, 0.85 }, +}) + +-------------------------------------------------------------------------------- +-- Shared state table (avoids excessive upvalues) +-------------------------------------------------------------------------------- +local S = { + frame = nil, -- MainFrame + inboxRows = {}, + currentTab = 1, + inboxPage = 1, + inboxChecked = {}, + collectQueue = {}, + collectTimer = nil, + isCollecting = false, + sendQueue = {}, + isSending = false, + collectElapsed = 0, + multiSend = nil, -- active multi-send state table +} + +local L = { + W = 360, H = 480, HEADER = 34, PAD = 12, TAB_H = 28, + BOTTOM = 46, ROWS = 8, ROW_H = 38, ICON = 30, MAX_SEND = 12, +} + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) +end + +local function CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.45) + s:SetBackdropBorderColor(0, 0, 0, 0.6) + s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) + return s +end + +local function FormatMoneyString(copper) + if not copper or copper <= 0 then return "0|cFFB87333c|r" end + local g = math.floor(copper / 10000) + local sv = math.floor(math.mod(copper, 10000) / 100) + local c = math.mod(copper, 100) + local parts = "" + if g > 0 then parts = parts .. "|cFFFFD700" .. g .. "g|r " end + if sv > 0 then parts = parts .. "|cFFC7C7CF" .. sv .. "s|r " end + if c > 0 then parts = parts .. "|cFFB87333" .. c .. "c|r" end + return parts +end + +local function CreateMoneyIcons(parent, fontSize, iconSize) + fontSize = fontSize or 10 + iconSize = iconSize or 11 + local font = GetFont() + local mf = CreateFrame("Frame", nil, parent) + mf:SetWidth(100); mf:SetHeight(iconSize + 2) + + local function MakePair(mfRef, r, g, b, tcL, tcR) + local txt = mfRef:CreateFontString(nil, "OVERLAY") + txt:SetFont(font, fontSize, "OUTLINE"); txt:SetTextColor(r, g, b) + local tex = mfRef:CreateTexture(nil, "ARTWORK") + tex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") + tex:SetTexCoord(tcL, tcR, 0, 1); tex:SetWidth(iconSize); tex:SetHeight(iconSize) + return txt, tex + end + mf.gTxt, mf.gTex = MakePair(mf, 1, 0.84, 0, 0, 0.25) + mf.sTxt, mf.sTex = MakePair(mf, 0.78, 0.78, 0.81, 0.25, 0.5) + mf.cTxt, mf.cTex = MakePair(mf, 0.72, 0.45, 0.2, 0.5, 0.75) + + function mf:SetMoney(copper) + copper = copper or 0 + local gv = math.floor(copper / 10000) + local sv = math.floor(math.mod(copper, 10000) / 100) + local cv = math.mod(copper, 100) + self.gTxt:Hide(); self.gTex:Hide(); self.gTxt:ClearAllPoints() + self.sTxt:Hide(); self.sTex:Hide(); self.sTxt:ClearAllPoints() + self.cTxt:Hide(); self.cTex:Hide(); self.cTxt:ClearAllPoints() + self.gTex:ClearAllPoints(); self.sTex:ClearAllPoints(); self.cTex:ClearAllPoints() + if copper <= 0 then return end + local anchor = nil + local function Attach(txt, tex, val) + txt:SetText(val) + txt:ClearAllPoints(); tex:ClearAllPoints() + if anchor then txt:SetPoint("LEFT", anchor, "RIGHT", 3, 0) + else txt:SetPoint("LEFT", self, "LEFT", 0, 0) end + tex:SetPoint("LEFT", txt, "RIGHT", 1, 0) + txt:Show(); tex:Show() + anchor = tex + end + if gv > 0 then Attach(self.gTxt, self.gTex, gv) end + if sv > 0 then Attach(self.sTxt, self.sTex, sv) end + if cv > 0 then Attach(self.cTxt, self.cTex, cv) end + end + return mf +end + +local function FormatExpiry(daysLeft) + if not daysLeft then return "" end + local d = math.floor(daysLeft) + if d <= 0 then return "|cFFFF3333即将过期|r" end + if d == 1 then return "|cFFFF6633剩余 1 天|r" end + if d <= 3 then return "|cFFFFAA33剩余 " .. d .. " 天|r" end + return "|cFF88FF88" .. d .. " 天|r" +end + +-------------------------------------------------------------------------------- +-- Action Button Factory +-------------------------------------------------------------------------------- +local function CreateActionBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 100) + btn:SetHeight(26) + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 11, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text) + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + btn.label = fs + + btn.disabled = false + function btn:SetDisabled(flag) + self.disabled = flag + if flag then + self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) + else + self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + end + end + + btn:SetScript("OnEnter", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end + end) + btn:SetScript("OnLeave", function() + if not this.disabled then + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end) + btn:SetScript("OnMouseDown", function() + if not this.disabled then + this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) + end + end) + btn:SetScript("OnMouseUp", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + end + end) + + return btn +end + +-------------------------------------------------------------------------------- +-- Tab Button Factory +-------------------------------------------------------------------------------- +local function CreateTabBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 70) + btn:SetHeight(22) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + btn:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) + btn:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 12, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text) + fs:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + btn.label = fs + + btn:SetScript("OnEnter", function() + if not this.active then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + end + end) + btn:SetScript("OnLeave", function() + if this.active then + this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) + this:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) + else + this:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) + this:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) + end + end) + + function btn:SetActive(flag) + self.active = flag + if flag then + self:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) + self:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) + self.label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + else + self:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) + self:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) + self.label:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + end + end + + return btn +end + +-------------------------------------------------------------------------------- +-- Checkbox Factory +-------------------------------------------------------------------------------- +local function CreateSmallCheckbox(parent, size) + local sz = size or 16 + local cb = CreateFrame("Button", nil, parent) + cb:SetWidth(sz); cb:SetHeight(sz) + cb:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + cb:SetBackdropColor(T.checkBg[1], T.checkBg[2], T.checkBg[3], T.checkBg[4]) + cb:SetBackdropBorderColor(T.checkBorder[1], T.checkBorder[2], T.checkBorder[3], T.checkBorder[4]) + local mark = cb:CreateTexture(nil, "OVERLAY") + mark:SetTexture("Interface\\Buttons\\UI-CheckBox-Check") + mark:SetAllPoints(cb); mark:Hide() + cb.mark = mark; cb.checked = false + function cb:SetChecked(flag) + self.checked = flag + if flag then self.mark:Show(); self:SetBackdropColor(T.checkFill[1], T.checkFill[2], T.checkFill[3], 0.3) + else self.mark:Hide(); self:SetBackdropColor(T.checkBg[1], T.checkBg[2], T.checkBg[3], T.checkBg[4]) end + end + function cb:IsChecked() return self.checked end + cb:SetScript("OnClick", function() + this:SetChecked(not this.checked) + if this.onToggle then this.onToggle(this.checked) end + end) + cb:SetScript("OnEnter", function() this:SetBackdropBorderColor(T.checkHoverBorder[1], T.checkHoverBorder[2], T.checkHoverBorder[3], T.checkHoverBorder[4]) end) + cb:SetScript("OnLeave", function() this:SetBackdropBorderColor(T.checkBorder[1], T.checkBorder[2], T.checkBorder[3], T.checkBorder[4]) end) + return cb +end + +-------------------------------------------------------------------------------- +-- Styled EditBox Factory +-------------------------------------------------------------------------------- +local function CreateStyledEditBox(parent, width, height, numeric) + local eb = CreateFrame("EditBox", nil, parent) + eb:SetWidth(width or 120); eb:SetHeight(height or 22) + eb:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + eb:SetBackdropColor(T.inputBg[1], T.inputBg[2], T.inputBg[3], T.inputBg[4] or 0.95) + eb:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], 0.8) + eb:SetFont(GetFont(), 11, "OUTLINE") + eb:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + eb:SetJustifyH("LEFT"); eb:SetAutoFocus(false); eb:SetTextInsets(6, 6, 2, 2) + if numeric then eb:SetNumeric(true) end + return eb +end + +-------------------------------------------------------------------------------- +-- INBOX: Row Factory +-------------------------------------------------------------------------------- +local function CreateInboxRow(parent, index) + local row = CreateFrame("Frame", "SFramesMailInboxRow" .. index, parent) + row:SetWidth(L.W - L.PAD * 2); row:SetHeight(L.ROW_H) + local bg = row:CreateTexture(nil, "BACKGROUND") + bg:SetTexture("Interface\\Buttons\\WHITE8X8"); bg:SetAllPoints(row) + local normalR, normalG, normalB, normalA + if math.mod(index, 2) == 0 then + normalR, normalG, normalB, normalA = T.rowNormal[1], T.rowNormal[2], T.rowNormal[3], T.rowNormal[4] + else + normalR, normalG, normalB, normalA = T.panelBg[1], T.panelBg[2], T.panelBg[3], 0.3 + end + bg:SetVertexColor(normalR, normalG, normalB, normalA) + row.bg = bg + + local hl = row:CreateTexture(nil, "ARTWORK") + hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + hl:SetBlendMode("ADD"); hl:SetAllPoints(row); hl:SetAlpha(0); hl:Hide() + row.highlight = hl + + row:EnableMouse(false) + + local cb = CreateSmallCheckbox(row, 16) + cb:SetPoint("LEFT", row, "LEFT", 4, 0) + cb:SetFrameLevel(row:GetFrameLevel() + 10) + row.checkbox = cb + + local iconFrame = CreateFrame("Frame", nil, row) + iconFrame:SetWidth(L.ICON); iconFrame:SetHeight(L.ICON) + iconFrame:SetPoint("LEFT", cb, "RIGHT", 6, 0) + iconFrame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + iconFrame:EnableMouse(true) + iconFrame:SetScript("OnEnter", function() + if not row.mailIndex then return end + row.highlight:SetAlpha(0.1); row.highlight:Show() + GameTooltip:SetOwner(iconFrame, "ANCHOR_RIGHT") + pcall(GameTooltip.SetInboxItem, GameTooltip, row.mailIndex) + GameTooltip:Show() + end) + iconFrame:SetScript("OnLeave", function() + row.highlight:SetAlpha(0); row.highlight:Hide() + GameTooltip:Hide() + end) + iconFrame:SetScript("OnMouseUp", function() + if row.mailIndex then ML:ShowMailDetail(row.mailIndex) end + end) + row.iconFrame = iconFrame + + local icon = iconFrame:CreateTexture(nil, "ARTWORK") + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:SetPoint("TOPLEFT", 3, -3); icon:SetPoint("BOTTOMRIGHT", -3, 3) + row.icon = icon + + local font = GetFont() + local senderFS = row:CreateFontString(nil, "OVERLAY") + senderFS:SetFont(font, 11, "OUTLINE") + senderFS:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 6, -2) + senderFS:SetWidth(110); senderFS:SetJustifyH("LEFT") + senderFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + row.senderFS = senderFS + + local subjectFS = row:CreateFontString(nil, "OVERLAY") + subjectFS:SetFont(font, 9, "OUTLINE") + subjectFS:SetPoint("TOPLEFT", senderFS, "BOTTOMLEFT", 0, -1) + subjectFS:SetWidth(110); subjectFS:SetJustifyH("LEFT") + subjectFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + row.subjectFS = subjectFS + + local moneyFrame = CreateMoneyIcons(row, 9, 10) + moneyFrame:SetPoint("RIGHT", row, "RIGHT", -52, 6) + row.moneyFrame = moneyFrame + + local codFS = row:CreateFontString(nil, "OVERLAY") + codFS:SetFont(font, 9, "OUTLINE"); codFS:SetTextColor(1, 0.33, 0.33) + codFS:SetPoint("RIGHT", moneyFrame, "LEFT", -2, 0) + codFS:Hide() + row.codFS = codFS + + local expiryFS = row:CreateFontString(nil, "OVERLAY") + expiryFS:SetFont(font, 9, "OUTLINE") + expiryFS:SetPoint("RIGHT", row, "RIGHT", -52, -6); expiryFS:SetJustifyH("RIGHT") + row.expiryFS = expiryFS + + local takeBtn = CreateActionBtn(row, "收取", 40) + takeBtn:SetHeight(17); takeBtn:SetPoint("TOPRIGHT", row, "TOPRIGHT", -6, -3) + takeBtn:SetFrameLevel(row:GetFrameLevel() + 10) + row.takeBtn = takeBtn + + local returnBtn = CreateActionBtn(row, "退回", 40) + returnBtn:SetHeight(17); returnBtn:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -6, 3) + returnBtn:SetFrameLevel(row:GetFrameLevel() + 10) + row.returnBtn = returnBtn + + row.mailIndex = nil + return row +end + +-------------------------------------------------------------------------------- +-- INBOX: Update +-------------------------------------------------------------------------------- +local function UpdateInbox() + if not S.frame or not S.frame:IsVisible() or S.currentTab ~= 1 then return end + local numItems = GetInboxNumItems() + local totalPages = math.max(1, math.ceil(numItems / L.ROWS)) + if S.inboxPage > totalPages then S.inboxPage = totalPages end + if S.inboxPage < 1 then S.inboxPage = 1 end + + S.frame.inboxPageText:SetText(string.format("第 %d / %d 页 (%d 封)", S.inboxPage, totalPages, numItems)) + S.frame.inboxPrevBtn:SetDisabled(S.inboxPage <= 1) + S.frame.inboxNextBtn:SetDisabled(S.inboxPage >= totalPages) + + for i = 1, L.ROWS do + local row = S.inboxRows[i] + local mi = (S.inboxPage - 1) * L.ROWS + i + if mi <= numItems then + local _, _, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead = GetInboxHeaderInfo(mi) + row.mailIndex = mi + if hasItem then + local _, itemTex = GetInboxItem(mi) + row.icon:SetTexture(itemTex or "Interface\\Icons\\INV_Misc_Note_01") + elseif money and money > 0 then + row.icon:SetTexture("Interface\\Icons\\INV_Misc_Coin_01") + else + row.icon:SetTexture("Interface\\Icons\\INV_Misc_Note_01") + end + row.senderFS:SetText(sender or "未知") + if not wasRead then + row.senderFS:SetTextColor(T.unreadMark[1], T.unreadMark[2], T.unreadMark[3]) + else + row.senderFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + end + row.subjectFS:SetText(subject or "") + row.codFS:Hide(); row.codFS:SetText("") + if money and money > 0 then + row.moneyFrame:SetMoney(money) + elseif CODAmount and CODAmount > 0 then + row.codFS:SetText("COD:"); row.codFS:Show() + row.moneyFrame:SetMoney(CODAmount) + else + row.moneyFrame:SetMoney(0) + end + row.expiryFS:SetText(FormatExpiry(daysLeft)) + local canTake = (hasItem or (money and money > 0)) and (not CODAmount or CODAmount == 0) + row.takeBtn:SetDisabled(not canTake) + row.takeBtn:SetScript("OnClick", function() + if row.mailIndex then + if hasItem then + TakeInboxItem(row.mailIndex) + elseif money and money > 0 then + local idx = row.mailIndex + TakeInboxMoney(idx) + if not S.deleteTimer then S.deleteTimer = CreateFrame("Frame") end + S.deleteElapsed = 0 + S.deleteTimer:SetScript("OnUpdate", function() + S.deleteElapsed = S.deleteElapsed + arg1 + if S.deleteElapsed >= 0.5 then + this:SetScript("OnUpdate", nil) + if idx <= GetInboxNumItems() then DeleteInboxItem(idx) end + end + end) + end + end + end) + row.returnBtn:SetScript("OnClick", function() + if row.mailIndex then ReturnInboxItem(row.mailIndex) end + end) + row.checkbox:SetChecked(S.inboxChecked[mi] == true) + row.checkbox.onToggle = function(checked) + S.inboxChecked[mi] = checked or nil + local any = false + for _, v in pairs(S.inboxChecked) do if v then any = true; break end end + S.frame.collectSelectedBtn:SetDisabled(not any) + end + if CODAmount and CODAmount > 0 then + row.checkbox:SetChecked(false); row.checkbox.disabled = true; S.inboxChecked[mi] = nil + else + row.checkbox.disabled = false + end + row:Show() + else + row.mailIndex = nil; row:Hide() + end + end + + local hasChecked = false + for _, v in pairs(S.inboxChecked) do if v then hasChecked = true; break end end + S.frame.collectSelectedBtn:SetDisabled(not hasChecked and not S.isCollecting) + S.frame.collectAllBtn:SetDisabled(numItems == 0 and not S.isCollecting) + if S.isCollecting then + S.frame.collectAllBtn.label:SetText("收取中...") + S.frame.collectSelectedBtn.label:SetText("收取中...") + else + S.frame.collectAllBtn.label:SetText("全部收取") + S.frame.collectSelectedBtn.label:SetText("收取选中") + end +end + +-------------------------------------------------------------------------------- +-- INBOX: Batch Collect +-------------------------------------------------------------------------------- +local function StopCollecting() + S.isCollecting = false; S.collectQueue = {}; S.collectPendingDelete = nil + if S.collectTimer then S.collectTimer:SetScript("OnUpdate", nil) end + UpdateInbox() +end + +local function ProcessCollectQueue() + if S.collectPendingDelete then + local mi = S.collectPendingDelete + S.collectPendingDelete = nil + if mi <= GetInboxNumItems() then DeleteInboxItem(mi) end + return + end + if table.getn(S.collectQueue) == 0 then StopCollecting(); return end + local mi = table.remove(S.collectQueue, 1) + if mi > GetInboxNumItems() then ProcessCollectQueue(); return end + local _, _, _, _, money, CODAmount, _, hasItem = GetInboxHeaderInfo(mi) + if CODAmount and CODAmount > 0 then ProcessCollectQueue(); return end + if hasItem then TakeInboxItem(mi) + elseif money and money > 0 then + TakeInboxMoney(mi) + S.collectPendingDelete = mi + else DeleteInboxItem(mi) end +end + +local function StartCollecting(indices) + if S.isCollecting then return end + S.collectQueue = {} + for i = table.getn(indices), 1, -1 do table.insert(S.collectQueue, indices[i]) end + if table.getn(S.collectQueue) == 0 then return end + S.isCollecting = true; S.collectElapsed = 0 + if not S.collectTimer then S.collectTimer = CreateFrame("Frame") end + local nextDelay = 0 + S.collectTimer:SetScript("OnUpdate", function() + if not S.isCollecting then this:SetScript("OnUpdate", nil); return end + S.collectElapsed = S.collectElapsed + arg1 + if S.collectElapsed >= nextDelay then + S.collectElapsed = 0; nextDelay = 0.5; ProcessCollectQueue() + end + end) + UpdateInbox() +end + +local function CollectAll() + local n = GetInboxNumItems() + if n == 0 then return end + local idx = {} + for i = 1, n do + local _, _, _, _, _, COD = GetInboxHeaderInfo(i) + if not COD or COD == 0 then table.insert(idx, i) end + end + StartCollecting(idx) +end + +local function CollectSelected() + local idx = {} + for k, v in pairs(S.inboxChecked) do if v then table.insert(idx, k) end end + table.sort(idx) + if table.getn(idx) == 0 then return end + S.inboxChecked = {} + StartCollecting(idx) +end + +local function SelectAllInbox() + local n = GetInboxNumItems() + for i = 1, n do + local _, _, _, _, _, COD = GetInboxHeaderInfo(i) + if not COD or COD == 0 then S.inboxChecked[i] = true end + end + UpdateInbox() +end + +local function DeselectAllInbox() + S.inboxChecked = {}; UpdateInbox() +end + +-------------------------------------------------------------------------------- +-- SEND: Item Queue +-------------------------------------------------------------------------------- +local function AddSendItem(bag, slot) + if table.getn(S.sendQueue) >= L.MAX_SEND then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 发送队列已满 (" .. L.MAX_SEND .. " 件)") + return + end + local link = GetContainerItemLink(bag, slot) + if not link then return end + local texture, count = GetContainerItemInfo(bag, slot) + for i = 1, table.getn(S.sendQueue) do + if S.sendQueue[i].bag == bag and S.sendQueue[i].slot == slot then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 该物品已在发送列表中"); return + end + end + table.insert(S.sendQueue, { bag = bag, slot = slot, link = link, texture = texture, count = count or 1 }) +end + +-------------------------------------------------------------------------------- +-- SEND: Accept cursor drag (find the locked bag slot the cursor picked up) +-------------------------------------------------------------------------------- +local function AcceptCursorItem() + if not CursorHasItem() then return false end + for bag = 0, 4 do + local slots = GetContainerNumSlots(bag) + for slot = 1, slots do + local _, _, locked = GetContainerItemInfo(bag, slot) + if locked then + AddSendItem(bag, slot) + ClearCursor() + ML:UpdateSendPanel() + return true + end + end + end + ClearCursor() + return false +end + +local function RemoveSendItem(index) + if index >= 1 and index <= table.getn(S.sendQueue) then table.remove(S.sendQueue, index) end +end + +local function ClearSendItems() S.sendQueue = {} end + +local function FlashStatus(text, color, duration) + if not S.frame or not S.frame.sendStatus then return end + S.frame.sendStatus:SetText(text) + S.frame.sendStatus:SetTextColor(color[1], color[2], color[3]) + S.statusFadeElapsed = 0 + if not S.statusFadeTimer then S.statusFadeTimer = CreateFrame("Frame") end + S.statusFadeTimer:SetScript("OnUpdate", function() + S.statusFadeElapsed = S.statusFadeElapsed + arg1 + if S.statusFadeElapsed >= (duration or 3) then + S.frame.sendStatus:SetText("") + this:SetScript("OnUpdate", nil) + end + end) +end + +-------------------------------------------------------------------------------- +-- SEND: Multi-Send Logic +-------------------------------------------------------------------------------- +local function ResetSendForm() + if not S.frame then return end + ClearSendItems() + if S.frame.toEditBox then S.frame.toEditBox:SetText("") end + if S.frame.subjectEditBox then S.frame.subjectEditBox:SetText("") end + if S.frame.bodyEditBox then S.frame.bodyEditBox:SetText("") end + if S.frame.goldEB then S.frame.goldEB:SetText("0") end + if S.frame.silverEB then S.frame.silverEB:SetText("0") end + if S.frame.copperEB then S.frame.copperEB:SetText("0") end + ML:UpdateSendPanel() +end + +local function FinishMultiSend() + local ms = S.multiSend + S.multiSend = nil + S.isSending = false + if S.updateFrame then S.updateFrame:SetScript("OnUpdate", nil) end + ResetSendForm() + if S.frame and S.frame.sendBtn then + S.frame.sendBtn.label:SetText("发送"); S.frame.sendBtn:SetDisabled(false) + end + if ms then + DEFAULT_CHAT_FRAME:AddMessage("|cFF88FF88[Nanami-Mail]|r 所有 " .. ms.total .. " 封邮件已发送完成") + FlashStatus("全部发送完成!", T.successText, 3) + end +end + +local function AbortMultiSend(reason) + S.multiSend = nil + S.isSending = false + if S.updateFrame then S.updateFrame:SetScript("OnUpdate", nil) end + if S.frame and S.frame.sendBtn then + S.frame.sendBtn.label:SetText("发送"); S.frame.sendBtn:SetDisabled(false) + end + if S.frame and S.frame.sendStatus then S.frame.sendStatus:SetText("") end + if reason then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r " .. reason) + FlashStatus("发送失败!", T.errorText, 5) + end +end + +local function DoMultiSend(recipient, subject, body, money) + if S.isSending then return end + if not recipient or recipient == "" then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 请输入收件人名字"); return + end + if not subject or subject == "" then subject = "Mail" end + if S.frame and S.frame.sendBtn then + S.frame.sendBtn.label:SetText("发送中..."); S.frame.sendBtn:SetDisabled(true) + end + local items = {} + for i = 1, table.getn(S.sendQueue) do table.insert(items, S.sendQueue[i]) end + + -- No attachments: plain text / money mail + if table.getn(items) == 0 then + if money and money > 0 then SetSendMailMoney(money) end + SendMail(recipient, subject, body or "") + return + end + + S.multiSend = { + items = items, + recipient = recipient, + subject = subject or "", + body = body or "", + money = money, + total = table.getn(items), + sentCount = 0, + phase = "attach", -- "attach" → "wait_send" → "cooldown" → "attach" ... + elapsed = 0, + } + S.isSending = true + + if not S.updateFrame then S.updateFrame = CreateFrame("Frame") end + S.updateFrame:SetScript("OnUpdate", function() + local ms = S.multiSend + if not ms then this:SetScript("OnUpdate", nil); return end + ms.elapsed = ms.elapsed + arg1 + + --------------------------------------------------------------- + -- Phase: attach — pick next item, attach to Blizzard mail slot, then send + --------------------------------------------------------------- + if ms.phase == "attach" then + ms.sentCount = ms.sentCount + 1 + if ms.sentCount > ms.total then + FinishMultiSend() + return + end + + local item = ms.items[ms.sentCount] + if not item then FinishMultiSend(); return end + + if S.frame and S.frame.sendStatus then + if S.statusFadeTimer then S.statusFadeTimer:SetScript("OnUpdate", nil) end + S.frame.sendStatus:SetText("发送中 " .. ms.sentCount .. "/" .. ms.total) + S.frame.sendStatus:SetTextColor(T.successText[1], T.successText[2], T.successText[3]) + end + + -- Ensure Blizzard send tab is active + if MailFrameTab_OnClick then MailFrameTab_OnClick(2) end + + -- Check item still in bag + if not GetContainerItemLink(item.bag, item.slot) then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 第" .. ms.sentCount .. "件物品已不在背包,跳过") + ms.elapsed = 0 + return + end + + -- Attach: clear → pick up → place + _ClearCursor() + _ClickSendMailItemButton() + _ClearCursor() + _PickupContainerItem(item.bag, item.slot) + _ClickSendMailItemButton() + + -- Verify + if not GetSendMailItem() then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 第" .. ms.sentCount .. "件附件挂载失败,跳过") + _ClearCursor() + ms.elapsed = 0 + return + end + + -- Money only on first mail + if ms.sentCount == 1 and ms.money and ms.money > 0 then + SetSendMailMoney(ms.money) + end + + -- Send this single-attachment mail + ms.sendOk = false + ms.phase = "wait_send" + ms.elapsed = 0 + SendMail(ms.recipient, ms.subject, ms.body) + + --------------------------------------------------------------- + -- Phase: wait_send — waiting for MAIL_SEND_SUCCESS + --------------------------------------------------------------- + elseif ms.phase == "wait_send" then + if ms.sendOk then + ms.phase = "cooldown" + ms.elapsed = 0 + elseif ms.elapsed >= 15 then + AbortMultiSend("发送超时,已停止") + end + + --------------------------------------------------------------- + -- Phase: cooldown — let server & Blizzard UI fully reset + --------------------------------------------------------------- + elseif ms.phase == "cooldown" then + if ms.elapsed >= 0.6 then + ms.phase = "attach" + ms.elapsed = 0 + end + end + end) +end + +-------------------------------------------------------------------------------- +-- SEND: Panel Update +-------------------------------------------------------------------------------- +function ML:UpdateSendPanel() + if not S.frame or S.currentTab ~= 2 then return end + if not S.frame.sendItemSlots then return end + for i = 1, L.MAX_SEND do + local slot = S.frame.sendItemSlots[i] + if not slot then break end + local entry = S.sendQueue[i] + if entry then + slot.icon:SetTexture(entry.texture); slot.icon:Show() + slot.countFS:SetText(entry.count > 1 and entry.count or "") + slot.removeBtn:Show(); slot.hasItem = true + else + slot.icon:SetTexture(nil); slot.icon:Hide() + slot.countFS:SetText(""); slot.removeBtn:Hide(); slot.hasItem = false + end + end +end + +-------------------------------------------------------------------------------- +-- BUILD: Main frame + header + tabs +-------------------------------------------------------------------------------- +local function BuildMainFrame() + local f = CreateFrame("Frame", "SFramesMailFrame", UIParent) + f:SetWidth(L.W); f:SetHeight(L.H) + f:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 16, -104) + f:SetFrameStrata("HIGH"); f:SetToplevel(true) + f:EnableMouse(true); f:SetMovable(true); f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + SetRoundBackdrop(f); CreateShadow(f) + S.frame = f + + local font = GetFont() + local header = CreateFrame("Frame", nil, f) + header:SetPoint("TOPLEFT", 0, 0); header:SetPoint("TOPRIGHT", 0, 0); header:SetHeight(L.HEADER) + header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) + header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) + + local titleIco = SFrames:CreateIcon(header, "mail", 16) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("LEFT", header, "LEFT", L.PAD, 0) + titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) + + local titleFS = header:CreateFontString(nil, "OVERLAY") + titleFS:SetFont(font, 14, "OUTLINE"); titleFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) + titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]); titleFS:SetText("邮箱") + + local closeBtn = CreateFrame("Button", nil, header) + closeBtn:SetWidth(20); closeBtn:SetHeight(20); closeBtn:SetPoint("RIGHT", header, "RIGHT", -8, 0) + local closeTex = closeBtn:CreateTexture(nil, "ARTWORK") + closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") + closeTex:SetTexCoord(0.25, 0.375, 0, 0.125); closeTex:SetAllPoints() + closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) + closeBtn:SetScript("OnClick", function() S.frame:Hide() end) + closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end) + closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) + + local sep = f:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8"); sep:SetHeight(1) + sep:SetPoint("TOPLEFT", f, "TOPLEFT", 6, -L.HEADER) + sep:SetPoint("TOPRIGHT", f, "TOPRIGHT", -6, -L.HEADER) + sep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + local tabInbox = CreateTabBtn(f, "收件箱", 70) + tabInbox:SetPoint("TOPLEFT", f, "TOPLEFT", L.PAD, -(L.HEADER + 6)) + tabInbox:SetScript("OnClick", function() S.currentTab = 1; ML:ShowInboxPanel() end) + f.tabInbox = tabInbox + + local tabSend = CreateTabBtn(f, "发送", 70) + tabSend:SetPoint("LEFT", tabInbox, "RIGHT", 4, 0) + tabSend:SetScript("OnClick", function() S.currentTab = 2; ML:ShowSendPanel() end) + f.tabSend = tabSend + + f:Hide() +end + +-------------------------------------------------------------------------------- +-- BUILD: Inbox panel +-------------------------------------------------------------------------------- +local function BuildInboxPanel() + local f = S.frame + local panelTop = L.HEADER + 6 + L.TAB_H + 4 + local ip = CreateFrame("Frame", nil, f) + ip:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -panelTop) + ip:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) + f.inboxPanel = ip + + local font = GetFont() + + local selAll = CreateActionBtn(ip, "全选", 50) + selAll:SetHeight(20); selAll:SetPoint("TOPLEFT", ip, "TOPLEFT", L.PAD, 0) + selAll:SetScript("OnClick", function() SelectAllInbox() end) + + local desel = CreateActionBtn(ip, "取消全选", 66) + desel:SetHeight(20); desel:SetPoint("LEFT", selAll, "RIGHT", 4, 0) + desel:SetScript("OnClick", function() DeselectAllInbox() end) + + local pageFS = ip:CreateFontString(nil, "OVERLAY") + pageFS:SetFont(font, 10, "OUTLINE"); pageFS:SetPoint("LEFT", desel, "RIGHT", 12, 0) + pageFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + f.inboxPageText = pageFS + + for i = 1, L.ROWS do + local row = CreateInboxRow(ip, i) + row:SetPoint("TOPLEFT", ip, "TOPLEFT", L.PAD, -(26 + (i - 1) * L.ROW_H)) + S.inboxRows[i] = row + end + + local bsep = ip:CreateTexture(nil, "ARTWORK") + bsep:SetTexture("Interface\\Buttons\\WHITE8X8"); bsep:SetHeight(1) + bsep:SetPoint("BOTTOMLEFT", ip, "BOTTOMLEFT", 6, L.BOTTOM) + bsep:SetPoint("BOTTOMRIGHT", ip, "BOTTOMRIGHT", -6, L.BOTTOM) + bsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + local prev = CreateActionBtn(ip, "<", 28) + prev:SetHeight(22); prev:SetPoint("BOTTOMLEFT", ip, "BOTTOMLEFT", L.PAD, 12) + prev:SetScript("OnClick", function() S.inboxPage = S.inboxPage - 1; UpdateInbox() end) + f.inboxPrevBtn = prev + + local nxt = CreateActionBtn(ip, ">", 28) + nxt:SetHeight(22); nxt:SetPoint("LEFT", prev, "RIGHT", 4, 0) + nxt:SetScript("OnClick", function() S.inboxPage = S.inboxPage + 1; UpdateInbox() end) + f.inboxNextBtn = nxt + + local colSel = CreateActionBtn(ip, "收取选中", 80) + colSel:SetHeight(24); colSel:SetPoint("BOTTOMRIGHT", ip, "BOTTOMRIGHT", -L.PAD, 10) + colSel:SetScript("OnClick", function() + if S.isCollecting then StopCollecting() else CollectSelected() end + end) + f.collectSelectedBtn = colSel + + local colAll = CreateActionBtn(ip, "全部收取", 80) + colAll:SetHeight(24); colAll:SetPoint("RIGHT", colSel, "LEFT", -6, 0) + colAll:SetScript("OnClick", function() + if S.isCollecting then StopCollecting() else CollectAll() end + end) + f.collectAllBtn = colAll +end + +-------------------------------------------------------------------------------- +-- BUILD: Mail detail panel (overlays inbox when a mail is clicked) +-------------------------------------------------------------------------------- +local function BuildDetailPanel() + local f = S.frame + local panelTop = L.HEADER + 6 + L.TAB_H + 4 + local dp = CreateFrame("Frame", nil, f) + dp:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -panelTop) + dp:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) + dp:Hide() + f.detailPanel = dp + + local font = GetFont() + local pad = L.PAD + + local backBtn = CreateActionBtn(dp, "< 返回", 60) + backBtn:SetHeight(20); backBtn:SetPoint("TOPLEFT", dp, "TOPLEFT", pad, 0) + backBtn:SetScript("OnClick", function() ML:HideMailDetail() end) + + local senderFS = dp:CreateFontString(nil, "OVERLAY") + senderFS:SetFont(font, 11, "OUTLINE"); senderFS:SetJustifyH("LEFT") + senderFS:SetPoint("TOPLEFT", dp, "TOPLEFT", pad, -26) + senderFS:SetWidth(L.W - pad * 2) + senderFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + dp.senderFS = senderFS + + local subjectFS = dp:CreateFontString(nil, "OVERLAY") + subjectFS:SetFont(font, 11, "OUTLINE"); subjectFS:SetJustifyH("LEFT") + subjectFS:SetPoint("TOPLEFT", senderFS, "BOTTOMLEFT", 0, -4) + subjectFS:SetWidth(L.W - pad * 2) + subjectFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + dp.subjectFS = subjectFS + + local sep1 = dp:CreateTexture(nil, "ARTWORK") + sep1:SetTexture("Interface\\Buttons\\WHITE8X8"); sep1:SetHeight(1) + sep1:SetPoint("TOPLEFT", subjectFS, "BOTTOMLEFT", 0, -6) + sep1:SetPoint("RIGHT", dp, "RIGHT", -pad, 0) + sep1:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + -- Body scroll frame + local bsf = CreateFrame("ScrollFrame", "SFramesMailDetailScroll", dp, "UIPanelScrollFrameTemplate") + bsf:SetPoint("TOPLEFT", sep1, "BOTTOMLEFT", 0, -4) + bsf:SetWidth(L.W - pad * 2 - 24); bsf:SetHeight(180) + + local bodyFS = CreateFrame("Frame", nil, bsf) + bodyFS:SetWidth(L.W - pad * 2 - 36); bodyFS:SetHeight(400) + local bodyText = bodyFS:CreateFontString(nil, "OVERLAY") + bodyText:SetFont(font, 11, "OUTLINE") + bodyText:SetPoint("TOPLEFT", 0, 0) + bodyText:SetWidth(L.W - pad * 2 - 36); bodyText:SetJustifyH("LEFT"); bodyText:SetJustifyV("TOP") + bodyText:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + bsf:SetScrollChild(bodyFS) + dp.bodyText = bodyText + dp.bodyFrame = bodyFS + + local sep2 = dp:CreateTexture(nil, "ARTWORK") + sep2:SetTexture("Interface\\Buttons\\WHITE8X8"); sep2:SetHeight(1) + sep2:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -4) + sep2:SetPoint("RIGHT", dp, "RIGHT", -pad, 0) + sep2:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + -- Item + money info line + local infoFS = dp:CreateFontString(nil, "OVERLAY") + infoFS:SetFont(font, 10, "OUTLINE"); infoFS:SetJustifyH("LEFT") + infoFS:SetPoint("TOPLEFT", sep2, "BOTTOMLEFT", 0, -6) + infoFS:SetWidth(L.W - pad * 2) + infoFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + dp.infoFS = infoFS + + local moneyLabel = dp:CreateFontString(nil, "OVERLAY") + moneyLabel:SetFont(font, 10, "OUTLINE"); moneyLabel:SetTextColor(1, 0.84, 0) + moneyLabel:SetPoint("TOPLEFT", infoFS, "BOTTOMLEFT", 0, -3) + moneyLabel:Hide() + dp.detailMoneyLabel = moneyLabel + + local detailMoney = CreateMoneyIcons(dp, 10, 11) + detailMoney:SetPoint("LEFT", moneyLabel, "RIGHT", 4, 0) + detailMoney:Hide() + dp.detailMoney = detailMoney + + local codLabel = dp:CreateFontString(nil, "OVERLAY") + codLabel:SetFont(font, 10, "OUTLINE"); codLabel:SetTextColor(1, 0.33, 0.33) + codLabel:SetPoint("LEFT", detailMoney, "RIGHT", 8, 0) + codLabel:Hide() + dp.detailCodLabel = codLabel + + local detailCod = CreateMoneyIcons(dp, 10, 11) + detailCod:SetPoint("LEFT", codLabel, "RIGHT", 4, 0) + detailCod:Hide() + dp.detailCod = detailCod + + -- Action buttons at bottom + local takeItemBtn = CreateActionBtn(dp, "收取物品", 72) + takeItemBtn:SetHeight(24); takeItemBtn:SetPoint("BOTTOMLEFT", dp, "BOTTOMLEFT", pad, 10) + dp.takeItemBtn = takeItemBtn + + local takeMoneyBtn = CreateActionBtn(dp, "收取金币", 72) + takeMoneyBtn:SetHeight(24); takeMoneyBtn:SetPoint("LEFT", takeItemBtn, "RIGHT", 4, 0) + dp.takeMoneyBtn = takeMoneyBtn + + local returnBtn = CreateActionBtn(dp, "退回", 50) + returnBtn:SetHeight(24); returnBtn:SetPoint("LEFT", takeMoneyBtn, "RIGHT", 4, 0) + dp.returnBtn = returnBtn + + local deleteBtn = CreateActionBtn(dp, "删除", 50) + deleteBtn:SetHeight(24); deleteBtn:SetPoint("LEFT", returnBtn, "RIGHT", 4, 0) + dp.deleteBtn = deleteBtn + + local replyBtn = CreateActionBtn(dp, "回复", 50) + replyBtn:SetHeight(24); replyBtn:SetPoint("BOTTOMRIGHT", dp, "BOTTOMRIGHT", -pad, 10) + dp.replyBtn = replyBtn +end + +-------------------------------------------------------------------------------- +-- Show / Hide mail detail +-------------------------------------------------------------------------------- +function ML:ShowMailDetail(mailIndex) + if not S.frame or not S.frame.detailPanel then return end + local dp = S.frame.detailPanel + S.detailMailIndex = mailIndex + + local _, _, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead = GetInboxHeaderInfo(mailIndex) + local bodyText = GetInboxText(mailIndex) + + dp.senderFS:SetText("发件人: |cFFFFFFFF" .. (sender or "未知") .. "|r " .. FormatExpiry(daysLeft)) + dp.subjectFS:SetText("主题: |cFFFFFFFF" .. (subject or "(无主题)") .. "|r") + + dp.bodyText:SetText(bodyText or "(无正文)") + local ok, textH = pcall(function() return dp.bodyText:GetStringHeight() end) + if not ok or not textH then textH = 40 end + dp.bodyFrame:SetHeight(math.max(textH + 10, 40)) + + local info = "" + if hasItem then + local itemName, itemTex = GetInboxItem(mailIndex) + if itemName then info = info .. "|cFFFFD700附件:|r " .. itemName end + end + if info == "" and (not money or money <= 0) and (not CODAmount or CODAmount <= 0) then + info = "无附件,无金币" + end + dp.infoFS:SetText(info) + + dp.detailMoneyLabel:Hide(); dp.detailMoney:Hide() + dp.detailCodLabel:Hide(); dp.detailCod:Hide() + if money and money > 0 then + dp.detailMoneyLabel:SetText("金额:"); dp.detailMoneyLabel:Show() + dp.detailMoney:SetMoney(money); dp.detailMoney:Show() + end + if CODAmount and CODAmount > 0 then + dp.detailCodLabel:SetText("COD:"); dp.detailCodLabel:Show() + dp.detailCod:SetMoney(CODAmount); dp.detailCod:Show() + end + + -- Take items button + local canTakeItem = hasItem and (not CODAmount or CODAmount == 0) + dp.takeItemBtn:SetDisabled(not canTakeItem) + dp.takeItemBtn:SetScript("OnClick", function() + if S.detailMailIndex then + TakeInboxItem(S.detailMailIndex) + end + end) + + -- Take money button + local canTakeMoney = money and money > 0 and (not CODAmount or CODAmount == 0) + dp.takeMoneyBtn:SetDisabled(not canTakeMoney) + dp.takeMoneyBtn:SetScript("OnClick", function() + if S.detailMailIndex then + local idx = S.detailMailIndex + local _, _, _, _, _, _, _, hi = GetInboxHeaderInfo(idx) + TakeInboxMoney(idx) + if not hi then + if not S.deleteTimer then S.deleteTimer = CreateFrame("Frame") end + S.deleteElapsed = 0 + S.deleteTimer:SetScript("OnUpdate", function() + S.deleteElapsed = S.deleteElapsed + arg1 + if S.deleteElapsed >= 0.5 then + this:SetScript("OnUpdate", nil) + if idx <= GetInboxNumItems() then DeleteInboxItem(idx) end + end + end) + end + ML:HideMailDetail() + end + end) + + -- Return button + dp.returnBtn:SetScript("OnClick", function() + if S.detailMailIndex then + ReturnInboxItem(S.detailMailIndex) + ML:HideMailDetail() + end + end) + + -- Delete button + dp.deleteBtn:SetScript("OnClick", function() + if S.detailMailIndex then + DeleteInboxItem(S.detailMailIndex) + ML:HideMailDetail() + end + end) + + -- Reply button + dp.replyBtn:SetScript("OnClick", function() + if sender then + S.currentTab = 2; ML:ShowSendPanel() + if S.frame.toEditBox then S.frame.toEditBox:SetText(sender) end + if S.frame.subjectEditBox then + local re = subject or "" + if string.sub(re, 1, 4) ~= "Re: " then re = "Re: " .. re end + S.frame.subjectEditBox:SetText(re) + end + end + end) + + S.frame.inboxPanel:Hide() + dp:Show() +end + +function ML:HideMailDetail() + if not S.frame then return end + S.detailMailIndex = nil + if S.frame.detailPanel then S.frame.detailPanel:Hide() end + S.frame.inboxPanel:Show() + UpdateInbox() +end + +-------------------------------------------------------------------------------- +-- BUILD: Send panel +-------------------------------------------------------------------------------- +local function BuildSendPanel() + local f = S.frame + local panelTop = L.HEADER + 6 + L.TAB_H + 4 + local sp = CreateFrame("Frame", "SFramesMailSendPanel", f) + sp:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -panelTop) + sp:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) + sp:EnableMouse(true) + sp:SetScript("OnReceiveDrag", function() AcceptCursorItem() end) + sp:SetScript("OnMouseUp", function() + if CursorHasItem() then AcceptCursorItem() end + end) + sp:Hide() + f.sendPanel = sp + + local font = GetFont() + + -- Recipient + local labelW = 50 + local ebW = L.W - L.PAD * 2 - labelW - 6 + local toLabel = sp:CreateFontString(nil, "OVERLAY") + toLabel:SetFont(font, 11, "OUTLINE"); toLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -6) + toLabel:SetWidth(labelW); toLabel:SetJustifyH("RIGHT") + toLabel:SetText("收件人:"); toLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3]) + local toEB = CreateStyledEditBox(sp, ebW, 22) + toEB:SetPoint("LEFT", toLabel, "RIGHT", 6, 0) + f.toEditBox = toEB + + -- Autocomplete dropdown for recipient + local AC_MAX = 8 + local acBox = CreateFrame("Frame", "SFramesMailAutoComplete", f) + acBox:SetWidth(ebW); acBox:SetFrameStrata("TOOLTIP") + acBox:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + acBox:SetBackdropColor(0.05, 0.05, 0.1, 0.95) + acBox:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.9) + acBox:SetPoint("TOPLEFT", toEB, "BOTTOMLEFT", 0, -2) + acBox:Hide() + acBox:EnableMouse(true) + f.acBox = acBox + + local acButtons = {} + for ai = 1, AC_MAX do + local btn = CreateFrame("Button", nil, acBox) + btn:SetHeight(18); btn:SetPoint("TOPLEFT", acBox, "TOPLEFT", 4, -4 - (ai - 1) * 18) + btn:SetPoint("RIGHT", acBox, "RIGHT", -4, 0) + btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + local bfs = btn:CreateFontString(nil, "OVERLAY") + bfs:SetFont(font, 11, "OUTLINE"); bfs:SetAllPoints(btn) + bfs:SetJustifyH("LEFT") + btn.label = bfs + btn.sourceName = nil + btn:SetScript("OnClick", function() + if btn.sourceName then + toEB:SetText(btn.sourceName) + end + acBox:Hide() + f.subjectEditBox:SetFocus() + end) + btn:Hide() + acButtons[ai] = btn + end + f.acButtons = acButtons + + local friendCache = {} + local function BuildFriendCache() + friendCache = {} + for fi = 1, GetNumFriends() do + local name = GetFriendInfo(fi) + if name then friendCache[name] = true end + end + end + + local function ShowSuggestions() + local input = toEB:GetText() + if not input or input == "" then acBox:Hide(); return end + local upper = string.upper(input) + + BuildFriendCache() + + local seen = { [UnitName("player")] = true } + local results = {} + + local function addName(name, source) + if not name or name == "" or seen[name] then return end + if string.find(string.upper(name), upper, 1, true) == 1 then + table.insert(results, { name = name, source = source }) + end + seen[name] = true + end + + for fi = 1, GetNumFriends() do + local name = GetFriendInfo(fi) + addName(name, "friend") + end + if GetNumGuildMembers then + for gi = 1, GetNumGuildMembers(true) do + local name = GetGuildRosterInfo(gi) + addName(name, "guild") + end + end + + local count = math.min(table.getn(results), AC_MAX) + if count == 0 then acBox:Hide(); return end + if count == 1 and results[1].name == input then acBox:Hide(); return end + + for ai = 1, AC_MAX do + local btn = acButtons[ai] + if ai <= count then + local r = results[ai] + local col = T.whoColor + local tag = "" + if r.source == "friend" then + col = T.friendColor; tag = " |cFF66FF88[好友]|r" + elseif r.source == "guild" then + col = T.guildColor; tag = " |cFF66CCFF[公会]|r" + end + btn.label:SetText("|cFF" .. string.format("%02x%02x%02x", + col[1] * 255, col[2] * 255, col[3] * 255) .. r.name .. "|r" .. tag) + btn.sourceName = r.name + btn:Show() + else + btn:Hide() + end + end + acBox:SetHeight(count * 18 + 8) + acBox:Show() + end + + toEB:SetScript("OnTextChanged", function() ShowSuggestions() end) + toEB:SetScript("OnEscapePressed", function() acBox:Hide(); this:ClearFocus() end) + toEB:SetScript("OnEnterPressed", function() + acBox:Hide(); f.subjectEditBox:SetFocus() + end) + + -- Subject (second line, aligned with recipient) + local subLabel = sp:CreateFontString(nil, "OVERLAY") + subLabel:SetFont(font, 11, "OUTLINE"); subLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -32) + subLabel:SetWidth(labelW); subLabel:SetJustifyH("RIGHT") + subLabel:SetText("主题:"); subLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3]) + local subEB = CreateStyledEditBox(sp, ebW, 22) + subEB:SetPoint("LEFT", subLabel, "RIGHT", 6, 0) + f.subjectEditBox = subEB + + toEB:SetScript("OnTabPressed", function() acBox:Hide(); f.subjectEditBox:SetFocus() end) + subEB:SetScript("OnTabPressed", function() f.toEditBox:SetFocus() end) + + -- Body + local bodyLabel = sp:CreateFontString(nil, "OVERLAY") + bodyLabel:SetFont(font, 11, "OUTLINE"); bodyLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -58) + bodyLabel:SetText("正文:"); bodyLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3]) + + local bsf = CreateFrame("ScrollFrame", "SFramesMailBodyScroll", sp, "UIPanelScrollFrameTemplate") + bsf:SetPoint("TOPLEFT", bodyLabel, "BOTTOMLEFT", 0, -4) + bsf:SetWidth(L.W - L.PAD * 2 - 28); bsf:SetHeight(100) + bsf:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + bsf:SetBackdropColor(T.inputBg[1], T.inputBg[2], T.inputBg[3], T.inputBg[4] or 0.95) + bsf:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], 0.8) + + local bodyEB = CreateFrame("EditBox", "SFramesMailBodyEditBox", bsf) + bodyEB:SetWidth(L.W - L.PAD * 2 - 40); bodyEB:SetHeight(200) + bodyEB:SetFont(font, 11, "OUTLINE"); bodyEB:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + bodyEB:SetAutoFocus(false); bodyEB:SetMultiLine(true); bodyEB:SetMaxLetters(500) + bodyEB:SetTextInsets(6, 6, 4, 4) + bsf:SetScrollChild(bodyEB) + f.bodyEditBox = bodyEB + + -- Money row + local mLabel = sp:CreateFontString(nil, "OVERLAY") + mLabel:SetFont(font, 11, "OUTLINE"); mLabel:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -10) + mLabel:SetText("附加金币:"); mLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3]) + + local gL = sp:CreateFontString(nil, "OVERLAY") + gL:SetFont(font, 10, "OUTLINE"); gL:SetPoint("LEFT", mLabel, "RIGHT", 6, 0) + gL:SetText("金"); gL:SetTextColor(T.moneyGold[1], T.moneyGold[2], T.moneyGold[3]) + local gEB = CreateStyledEditBox(sp, 60, 20, true); gEB:SetPoint("LEFT", gL, "RIGHT", 4, 0); gEB:SetText("0"); f.goldEB = gEB + + local sL = sp:CreateFontString(nil, "OVERLAY") + sL:SetFont(font, 10, "OUTLINE"); sL:SetPoint("LEFT", gEB, "RIGHT", 6, 0) + sL:SetText("银"); sL:SetTextColor(T.moneySilver[1], T.moneySilver[2], T.moneySilver[3]) + local sEB = CreateStyledEditBox(sp, 40, 20, true); sEB:SetPoint("LEFT", sL, "RIGHT", 4, 0); sEB:SetText("0"); f.silverEB = sEB + + local cL = sp:CreateFontString(nil, "OVERLAY") + cL:SetFont(font, 10, "OUTLINE"); cL:SetPoint("LEFT", sEB, "RIGHT", 6, 0) + cL:SetText("铜"); cL:SetTextColor(T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3]) + local cEB = CreateStyledEditBox(sp, 40, 20, true); cEB:SetPoint("LEFT", cL, "RIGHT", 4, 0); cEB:SetText("0"); f.copperEB = cEB + + -- Attachments + local aLabel = sp:CreateFontString(nil, "OVERLAY") + aLabel:SetFont(font, 11, "OUTLINE"); aLabel:SetPoint("TOPLEFT", mLabel, "TOPLEFT", 0, -28) + aLabel:SetText("附件 (右击/拖放背包物品添加):"); aLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3]) + + local clrBtn = CreateActionBtn(sp, "清空", 50) + clrBtn:SetHeight(20); clrBtn:SetPoint("LEFT", aLabel, "RIGHT", 8, 0) + clrBtn:SetScript("OnClick", function() ClearSendItems(); ML:UpdateSendPanel() end) + + -- Item slot grid + f.sendItemSlots = {} + BuildSendSlots(sp, aLabel, f) + + -- Send button & status + local status = sp:CreateFontString(nil, "OVERLAY") + status:SetFont(font, 11, "OUTLINE"); status:SetPoint("BOTTOMLEFT", sp, "BOTTOMLEFT", L.PAD, 14) + status:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + f.sendStatus = status + + local sendBtn = CreateActionBtn(sp, "发送", 80) + sendBtn:SetHeight(26); sendBtn:SetPoint("BOTTOMRIGHT", sp, "BOTTOMRIGHT", -L.PAD, 10) + sendBtn:SetScript("OnClick", function() + local r = f.toEditBox:GetText() + local sub = f.subjectEditBox:GetText() + local bd = f.bodyEditBox:GetText() + local g = tonumber(f.goldEB:GetText()) or 0 + local sv = tonumber(f.silverEB:GetText()) or 0 + local c = tonumber(f.copperEB:GetText()) or 0 + DoMultiSend(r, sub, bd, g * 10000 + sv * 100 + c) + end) + f.sendBtn = sendBtn +end + +-------------------------------------------------------------------------------- +-- BUILD: Send item slots (separate function to stay under upvalue limit) +-------------------------------------------------------------------------------- +function BuildSendSlots(parent, anchor, f) + local slotSize, slotGap, perRow = 34, 4, 6 + for i = 1, L.MAX_SEND do + local row = math.floor((i - 1) / perRow) + local col = math.mod((i - 1), perRow) + local sf = CreateFrame("Button", "SFramesMailSendSlot" .. i, parent) + sf:SetWidth(slotSize); sf:SetHeight(slotSize) + sf:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", col * (slotSize + slotGap), -(4 + row * (slotSize + slotGap))) + sf:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + sf:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + sf:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + + local ico = sf:CreateTexture(nil, "ARTWORK") + ico:SetTexCoord(0.08, 0.92, 0.08, 0.92) + ico:SetPoint("TOPLEFT", 3, -3); ico:SetPoint("BOTTOMRIGHT", -3, 3); ico:Hide() + sf.icon = ico + + local cnt = sf:CreateFontString(nil, "OVERLAY") + cnt:SetFont("Fonts\\ARIALN.TTF", 11, "OUTLINE") + cnt:SetPoint("BOTTOMRIGHT", sf, "BOTTOMRIGHT", -2, 2); cnt:SetJustifyH("RIGHT") + sf.countFS = cnt + + local rb = CreateFrame("Button", nil, sf) + rb:SetWidth(14); rb:SetHeight(14); rb:SetPoint("TOPRIGHT", sf, "TOPRIGHT", 2, 2) + local rtx = rb:CreateTexture(nil, "OVERLAY") + rtx:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") + rtx:SetTexCoord(0.25, 0.375, 0, 0.125); rtx:SetAllPoints(); rtx:SetVertexColor(1, 0.4, 0.4) + rb:Hide(); sf.removeBtn = rb; sf.hasItem = false + + local si = i + rb:SetScript("OnClick", function() RemoveSendItem(si); ML:UpdateSendPanel() end) + sf:SetScript("OnReceiveDrag", function() AcceptCursorItem() end) + sf:SetScript("OnClick", function() + if CursorHasItem() then AcceptCursorItem(); return end + if arg1 == "RightButton" and sf.hasItem then RemoveSendItem(si); ML:UpdateSendPanel() end + end) + sf:RegisterForClicks("LeftButtonUp", "RightButtonUp") + sf:SetScript("OnEnter", function() + this:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4]) + local e = S.sendQueue[si] + if e and e.link then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT"); pcall(GameTooltip.SetHyperlink, GameTooltip, e.link); GameTooltip:Show() + end + end) + sf:SetScript("OnLeave", function() + this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + GameTooltip:Hide() + end) + f.sendItemSlots[i] = sf + end +end + +-------------------------------------------------------------------------------- +-- BUILD: Events & hooks +-------------------------------------------------------------------------------- +local function SetupEvents() + local f = S.frame + + f:SetScript("OnHide", function() + StopCollecting(); if S.multiSend then AbortMultiSend() end; pcall(CloseMail) + end) + + f:SetScript("OnEvent", function() + if event == "MAIL_SHOW" then + if SFramesDB and SFramesDB.enableMail == false then return end + S.currentTab = 1; S.inboxPage = 1; S.inboxChecked = {} + CheckInbox(); f:Show(); ML:ShowInboxPanel() + elseif event == "MAIL_INBOX_UPDATE" then + if f:IsVisible() then + if S.detailMailIndex and f.detailPanel and f.detailPanel:IsVisible() then + if S.detailMailIndex <= GetInboxNumItems() then + ML:ShowMailDetail(S.detailMailIndex) + else + ML:HideMailDetail() + end + else + UpdateInbox() + end + end + elseif event == "MAIL_CLOSED" then + if S.multiSend then AbortMultiSend("邮箱已关闭") end + f:Hide() + elseif event == "MAIL_SEND_SUCCESS" then + if S.multiSend then + S.multiSend.sendOk = true + S.multiSend.phase = "cooldown" + S.multiSend.elapsed = 0 + else + FlashStatus("发送成功!", T.successText, 3) + ResetSendForm() + if f.sendBtn then f.sendBtn.label:SetText("发送"); f.sendBtn:SetDisabled(false) end + end + elseif event == "MAIL_SEND_INFO_UPDATE" then + -- noop + elseif event == "MAIL_FAILED" then + if S.multiSend and S.multiSend.phase == "wait_send" and not S.multiSend.sendOk then + AbortMultiSend("发送失败") + elseif not S.multiSend then + if f.sendBtn then f.sendBtn.label:SetText("发送"); f.sendBtn:SetDisabled(false) end + FlashStatus("发送失败!", T.errorText, 5) + end + end + end) + + f:RegisterEvent("MAIL_SHOW"); f:RegisterEvent("MAIL_INBOX_UPDATE") + f:RegisterEvent("MAIL_CLOSED"); f:RegisterEvent("MAIL_SEND_SUCCESS") + f:RegisterEvent("MAIL_SEND_INFO_UPDATE"); f:RegisterEvent("MAIL_FAILED") + + if MailFrame then + local origMailOnShow = MailFrame:GetScript("OnShow") + MailFrame:SetScript("OnShow", function() + if origMailOnShow then origMailOnShow() end + this:ClearAllPoints() + this:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000) + this:EnableMouse(false) + end) + for i = table.getn(UISpecialFrames), 1, -1 do + if UISpecialFrames[i] == "MailFrame" then + table.remove(UISpecialFrames, i) + end + end + end + if OpenMailFrame then + OpenMailFrame:UnregisterAllEvents(); OpenMailFrame:Hide() + end + + tinsert(UISpecialFrames, "SFramesMailFrame") + + SFrames.Mail.TryAddItemFromBag = function(bag, slot) + if S.frame and S.frame:IsVisible() and not S.isSending then + if bag and slot then + if S.currentTab ~= 2 then S.currentTab = 2; ML:ShowSendPanel() end + AddSendItem(bag, slot); ML:UpdateSendPanel() + return true + end + end + return false + end + + if ContainerFrameItemButton_OnClick then + local orig = ContainerFrameItemButton_OnClick + ContainerFrameItemButton_OnClick = function(button, ignoreShift) + if arg1 == "RightButton" then + if SFrames.Mail.TryAddItemFromBag(this:GetParent():GetID(), this:GetID()) then return end + end + orig(button, ignoreShift) + end + end +end + +-------------------------------------------------------------------------------- +-- Initialize (calls sub-builders) +-------------------------------------------------------------------------------- +function ML:Initialize() + if S.frame then return end + BuildMainFrame() + BuildInboxPanel() + BuildDetailPanel() + BuildSendPanel() + SetupEvents() +end + +-------------------------------------------------------------------------------- +-- Panel Switching +-------------------------------------------------------------------------------- +function ML:ShowInboxPanel() + if not S.frame then return end + S.frame.tabInbox:SetActive(true); S.frame.tabSend:SetActive(false) + if S.frame.detailPanel then S.frame.detailPanel:Hide() end + S.frame.inboxPanel:Show(); S.frame.sendPanel:Hide() + S.detailMailIndex = nil + UpdateInbox() +end + +function ML:ShowSendPanel() + if not S.frame then return end + S.frame.tabInbox:SetActive(false); S.frame.tabSend:SetActive(true) + if S.frame.detailPanel then S.frame.detailPanel:Hide() end + S.detailMailIndex = nil + S.frame.inboxPanel:Hide(); S.frame.sendPanel:Show() + if S.frame.sendStatus then S.frame.sendStatus:SetText("") end + if S.statusFadeTimer then S.statusFadeTimer:SetScript("OnUpdate", nil) end + ML:UpdateSendPanel() +end + +-------------------------------------------------------------------------------- +-- Bootstrap +-------------------------------------------------------------------------------- +local bootstrap = CreateFrame("Frame") +bootstrap:RegisterEvent("PLAYER_LOGIN") +bootstrap:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + if SFramesDB.enableMail == nil then SFramesDB.enableMail = true end + if SFramesDB.enableMail ~= false then ML:Initialize() end + end +end) diff --git a/MapIcons.lua b/MapIcons.lua new file mode 100644 index 0000000..09d56db --- /dev/null +++ b/MapIcons.lua @@ -0,0 +1,337 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: MapIcons - Class-colored party/raid icons on maps +-- Shows class icon circles on World Map, Battlefield Minimap, and Minimap +-- Uses UI-Classes-Circles.tga for class-specific circular portraits +-- Zone size data: prefers pfQuest DB, falls back to built-in table +-------------------------------------------------------------------------------- + +SFrames.MapIcons = SFrames.MapIcons or {} +local MI = SFrames.MapIcons + +local CLASS_ICON_PATH = "Interface\\AddOns\\Nanami-UI\\img\\UI-Classes-Circles" +local CLASS_ICON_TCOORDS = SFrames.CLASS_ICON_TCOORDS +local CLASS_COLORS = SFrames.Config.colors.class + +-------------------------------------------------------------------------------- +-- Minimap zoom yard ranges: [indoor/outdoor][zoomLevel] = diameter in yards +-- indoor=0, outdoor=1 +-------------------------------------------------------------------------------- +local MM_ZOOM = { + [0] = { [0]=300, [1]=240, [2]=180, [3]=120, [4]=80, [5]=50 }, + [1] = { [0]=466.67, [1]=400, [2]=333.33, [3]=266.67, [4]=200, [5]=133.33 }, +} + +-------------------------------------------------------------------------------- +-- Built-in zone sizes (width, height in yards) keyed by GetMapInfo() file name +-- Fallback when pfQuest is not installed +-------------------------------------------------------------------------------- +local ZONE_SIZES = { + ["Ashenvale"] = { 5766.67, 3843.75 }, + ["Aszhara"] = { 5533.33, 3689.58 }, + ["Darkshore"] = { 6550.00, 4366.66 }, + ["Desolace"] = { 4600.00, 3066.67 }, + ["Durotar"] = { 4925.00, 3283.34 }, + ["Dustwallow"] = { 5250.00, 3500.00 }, + ["Felwood"] = { 5750.00, 3833.33 }, + ["Feralas"] = { 6950.00, 4633.33 }, + ["Moonglade"] = { 2308.33, 1539.59 }, + ["Mulgore"] = { 5250.00, 3500.00 }, + ["Silithus"] = { 3483.33, 2322.92 }, + ["StonetalonMountains"] = { 4883.33, 3256.25 }, + ["Tanaris"] = { 6900.00, 4600.00 }, + ["Teldrassil"] = { 4925.00, 3283.34 }, + ["Barrens"] = { 10133.34, 6756.25 }, + ["ThousandNeedles"] = { 4400.00, 2933.33 }, + ["UngoroCrater"] = { 3677.08, 2452.08 }, + ["Winterspring"] = { 7100.00, 4733.33 }, + ["Alterac"] = { 2800.00, 1866.67 }, + ["ArathiHighlands"] = { 3600.00, 2400.00 }, + ["Badlands"] = { 2487.50, 1658.34 }, + ["BlastedLands"] = { 3350.00, 2233.30 }, + ["BurningSteppes"] = { 2929.16, 1952.08 }, + ["DeadwindPass"] = { 2500.00, 1666.63 }, + ["DunMorogh"] = { 4925.00, 3283.34 }, + ["Duskwood"] = { 2700.00, 1800.03 }, + ["EasternPlaguelands"] = { 4031.25, 2687.50 }, + ["ElwynnForest"] = { 3470.84, 2314.62 }, + ["Hilsbrad"] = { 3200.00, 2133.33 }, + ["Hinterlands"] = { 3850.00, 2566.67 }, + ["LochModan"] = { 2758.33, 1839.58 }, + ["RedridgeMountains"] = { 2170.84, 1447.90 }, + ["SearingGorge"] = { 1837.50, 1225.00 }, + ["SilverpineForest"] = { 4200.00, 2800.00 }, + ["Stranglethorn"] = { 6381.25, 4254.10 }, + ["SwampOfSorrows"] = { 2293.75, 1529.17 }, + ["Tirisfal"] = { 4518.75, 3012.50 }, + ["WesternPlaguelands"] = { 4300.00, 2866.67 }, + ["Westfall"] = { 3500.00, 2333.30 }, + ["Wetlands"] = { 4300.00, 2866.67 }, +} + +-------------------------------------------------------------------------------- +-- Indoor detection (CVar trick from pfQuest) +-- Returns 0 = indoor, 1 = outdoor +-------------------------------------------------------------------------------- +local cachedIndoor = 1 +local indoorCheckTime = 0 + +local function DetectIndoor() + if pfMap and pfMap.minimap_indoor then + return pfMap.minimap_indoor() + end + local ok1, zoomVal = pcall(GetCVar, "minimapZoom") + local ok2, insideVal = pcall(GetCVar, "minimapInsideZoom") + if not ok1 or not ok2 then return 1 end + local tempzoom = 0 + local state = 1 + if zoomVal == insideVal then + local cur = Minimap:GetZoom() + if cur >= 3 then + Minimap:SetZoom(cur - 1) + tempzoom = 1 + else + Minimap:SetZoom(cur + 1) + tempzoom = -1 + end + end + local ok3, zoomVal2 = pcall(GetCVar, "minimapZoom") + local ok4, insideVal2 = pcall(GetCVar, "minimapInsideZoom") + if ok3 and ok4 and zoomVal2 ~= insideVal2 then + state = 0 + end + if tempzoom ~= 0 then + Minimap:SetZoom(Minimap:GetZoom() + tempzoom) + end + return state +end + +-------------------------------------------------------------------------------- +-- Get current zone dimensions in yards +-------------------------------------------------------------------------------- +local function GetZoneYards() + if pfMap and pfMap.GetMapIDByName and pfDB and pfDB["minimap"] then + local name = GetRealZoneText and GetRealZoneText() or "" + if name ~= "" then + local id = pfMap:GetMapIDByName(name) + if id and pfDB["minimap"][id] then + return pfDB["minimap"][id][1], pfDB["minimap"][id][2] + end + end + end + if GetMapInfo then + local ok, info = pcall(GetMapInfo) + if ok and info and ZONE_SIZES[info] then + return ZONE_SIZES[info][1], ZONE_SIZES[info][2] + end + end + return nil, nil +end + +-------------------------------------------------------------------------------- +-- 1. World Map + Battlefield Minimap: class icon overlays +-------------------------------------------------------------------------------- +local mapButtons + +local function InitMapButtons() + if mapButtons then return end + mapButtons = {} + for i = 1, 4 do + mapButtons["WorldMapParty" .. i] = "party" .. i + mapButtons["BattlefieldMinimapParty" .. i] = "party" .. i + end + for i = 1, 40 do + mapButtons["WorldMapRaid" .. i] = "raid" .. i + mapButtons["BattlefieldMinimapRaid" .. i] = "raid" .. i + end +end + +local mapTickTime = 0 + +local function UpdateMapClassIcons() + mapTickTime = mapTickTime + (arg1 or 0) + if mapTickTime < 0.15 then return end + mapTickTime = 0 + + if not mapButtons then InitMapButtons() end + + local _G = getfenv(0) + for name, unit in pairs(mapButtons) do + local frame = _G[name] + if frame and frame:IsVisible() and UnitExists(unit) then + local defIcon = _G[name .. "Icon"] + if defIcon then defIcon:SetTexture() end + + if not frame.nanamiClassTex then + frame.nanamiClassTex = frame:CreateTexture(nil, "OVERLAY") + frame.nanamiClassTex:SetTexture(CLASS_ICON_PATH) + frame.nanamiClassTex:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, -2) + frame.nanamiClassTex:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -2, 2) + end + + local _, class = UnitClass(unit) + if class and CLASS_ICON_TCOORDS and CLASS_ICON_TCOORDS[class] then + local tc = CLASS_ICON_TCOORDS[class] + frame.nanamiClassTex:SetTexCoord(tc[1], tc[2], tc[3], tc[4]) + frame.nanamiClassTex:Show() + else + frame.nanamiClassTex:Hide() + end + end + end +end + +-------------------------------------------------------------------------------- +-- 2. Minimap: class-colored party dot overlays +-------------------------------------------------------------------------------- +local MAX_PARTY = 4 +local mmDots = {} + +local function CreateMinimapDot(index) + local dot = CreateFrame("Frame", "NanamiMMDot" .. index, Minimap) + dot:SetWidth(10) + dot:SetHeight(10) + dot:SetFrameStrata("MEDIUM") + dot:SetFrameLevel(Minimap:GetFrameLevel() + 5) + + dot.icon = dot:CreateTexture(nil, "ARTWORK") + dot.icon:SetTexture("Interface\\Minimap\\UI-Minimap-Background") + dot.icon:SetAllPoints() + dot.icon:SetVertexColor(1, 0.82, 0, 1) + + dot:Hide() + return dot +end + +local mmTickTime = 0 + +local function UpdateMinimapDots() + mmTickTime = mmTickTime + (arg1 or 0) + if mmTickTime < 0.25 then return end + mmTickTime = 0 + + if not Minimap or not Minimap:IsVisible() then + for i = 1, MAX_PARTY do + if mmDots[i] then mmDots[i]:Hide() end + end + return + end + + local numParty = GetNumPartyMembers and GetNumPartyMembers() or 0 + if numParty == 0 then + for i = 1, MAX_PARTY do + if mmDots[i] then mmDots[i]:Hide() end + end + return + end + + if WorldMapFrame and WorldMapFrame:IsVisible() then + return + end + + if SetMapToCurrentZone then + pcall(SetMapToCurrentZone) + end + + local px, py = GetPlayerMapPosition("player") + if not px or not py or (px == 0 and py == 0) then + for i = 1, MAX_PARTY do + if mmDots[i] then mmDots[i]:Hide() end + end + return + end + + local zw, zh = GetZoneYards() + if not zw or not zh or zw == 0 or zh == 0 then + for i = 1, MAX_PARTY do + if mmDots[i] then mmDots[i]:Hide() end + end + return + end + + local now = GetTime() + if now - indoorCheckTime > 3 then + indoorCheckTime = now + cachedIndoor = DetectIndoor() + end + + local zoom = Minimap:GetZoom() + local mmYards = MM_ZOOM[cachedIndoor] and MM_ZOOM[cachedIndoor][zoom] + or MM_ZOOM[1][zoom] or 466.67 + local mmHalfYards = mmYards / 2 + local mmHalfPx = Minimap:GetWidth() / 2 + + local facing = 0 + local doRotate = false + local okCvar, rotateVal = pcall(GetCVar, "rotateMinimap") + if okCvar and rotateVal == "1" and GetPlayerFacing then + local ok2, f = pcall(GetPlayerFacing) + if ok2 and f then + facing = f + doRotate = true + end + end + + for i = 1, MAX_PARTY do + local unit = "party" .. i + if i <= numParty and UnitExists(unit) and UnitIsConnected(unit) then + local mx, my = GetPlayerMapPosition(unit) + if mx and my and (mx ~= 0 or my ~= 0) then + local dx = (mx - px) * zw + local dy = (py - my) * zh + + if doRotate then + local s = math.sin(facing) + local c = math.cos(facing) + dx, dy = dx * c + dy * s, -dx * s + dy * c + end + + local dist = math.sqrt(dx * dx + dy * dy) + if dist < mmHalfYards * 0.92 then + local scale = mmHalfPx / mmHalfYards + + if not mmDots[i] then + mmDots[i] = CreateMinimapDot(i) + end + local dot = mmDots[i] + + local _, class = UnitClass(unit) + local cc = class and CLASS_COLORS and CLASS_COLORS[class] + if cc then + dot.icon:SetVertexColor(cc.r, cc.g, cc.b, 1) + else + dot.icon:SetVertexColor(1, 0.82, 0, 1) + end + + dot:ClearAllPoints() + dot:SetPoint("CENTER", Minimap, "CENTER", dx * scale, dy * scale) + dot:Show() + else + if mmDots[i] then mmDots[i]:Hide() end + end + else + if mmDots[i] then mmDots[i]:Hide() end + end + else + if mmDots[i] then mmDots[i]:Hide() end + end + end +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function MI:Initialize() + InitMapButtons() + + local updater = CreateFrame("Frame", "NanamiMapIconsUpdater", UIParent) + updater._elapsed = 0 + updater:SetScript("OnUpdate", function() + this._elapsed = (this._elapsed or 0) + arg1 + if this._elapsed < 0.2 then return end + this._elapsed = 0 + UpdateMapClassIcons() + UpdateMinimapDots() + end) + + SFrames:Print("地图职业图标模块已加载") +end diff --git a/MapReveal.lua b/MapReveal.lua new file mode 100644 index 0000000..6105df0 --- /dev/null +++ b/MapReveal.lua @@ -0,0 +1,287 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: MapReveal -- Reveal unexplored world map areas +-- Adapted from ShaguTweaks-extras worldmap-reveal approach +-- Uses LibMapOverlayData (from !Libs) supplemented with Turtle WoW zones +-------------------------------------------------------------------------------- + +SFrames.MapReveal = SFrames.MapReveal or {} + +local MapReveal = SFrames.MapReveal +local origWorldMapFrame_Update = nil +local overlayDBPatched = false + +local errata = { + ["Interface\\WorldMap\\Tirisfal\\BRIGHTWATERLAKE"] = { offsetX = { 587, 584 } }, + ["Interface\\WorldMap\\Silverpine\\BERENSPERIL"] = { offsetY = { 417, 415 } }, +} + +-- Turtle WoW new/modified zones not present in LibMapOverlayData +local TurtleWoW_Zones = { + ["StonetalonMountains"] = { + "SUNROCKRETREAT:512:256:256:256", "WINDSHEARCRAG:256:256:512:256", + "MIRKFALLONLAKE:512:512:256:0", "THECHARREDVALE:256:512:256:256", + "STONETALONPEAK:256:256:256:0", "WEBWINDERPATH:256:512:512:256", + "AMANIALOR:512:256:0:0", "GRIMTOTEMPOST:512:256:512:512", + "CAMPAPARAJE:512:256:512:512", "MALAKAJIN:512:256:512:512", + "BOULDERSLIDERAVINE:256:256:512:512", "SISHIRCANYON:256:512:512:256", + "VENTURECOMPANYCAMP:256:512:256:0", "BLACKSANDOILFIELDS:512:512:0:0", + "POWDERTOWN:256:256:256:256", "BRAMBLETHORNPASS:512:512:512:256", + "BAELHARDUL:512:256:512:256", "BROKENCLIFFMINE:256:512:256:0", + "THEEARTHENRING:512:256:256:256", + }, + ["UpperKarazhan2f"] = { + "OUTLAND:1024:768:0:0", + }, + ["GrimReaches"] = { + "DUNKITHAS:512:256:256:256", "THEGRIMHOLLOW:512:512:256:256", + "LAKEKITHAS:512:256:256:256", "SLATEBEARDSFORGE:512:256:256:256", + "THEHIGHPASS:256:256:256:256", "SALGAZMINES:256:256:512:256", + "EASTRIDGEOUTPOST:512:512:256:0", "BAGGOTHSRAMPART:256:512:256:0", + "RUINSOFSTOLGAZKEEP:256:256:256:0", "GROLDANSEXCAVATION:256:512:512:0", + "ZARMGETHSTRONGHOLD:512:256:256:0", "GETHKAR:512:256:256:0", + "ZARMGETHPOINT:512:256:256:0", "SHATTERBLADEPOST:256:256:512:0", + "BRANGARSFOLLY:256:256:512:0", "BARLEYCRESTFARMSTEAD:512:512:256:0", + }, + ["Balor"] = { + "GULLWINGWRECKAGE:512:256:0:0", "BILGERATCOMPOUND:256:256:256:0", + "SIOUTPOST:256:512:512:256", "RUINSOFBREEZEHAVEN:512:256:256:256", + "CROAKINGPLATEAU:512:512:256:0", "LANGSTONORCHARD:256:256:256:256", + "SORROWMORELAKE:256:256:256:256", "SCURRYINGTHICKET:256:512:256:0", + "STORMWROUGHTCASTLE:512:256:256:256", "STORMREAVERSPIRE:512:512:256:256", + "WINDROCKCLIFFS:256:512:256:256", "TREACHEROUSCRAGS:512:512:256:256", + "VANDERFARMSTEAD:256:256:256:256", "GRAHANESTATE:256:256:256:256", + "STORMBREAKERPOINT:512:512:512:0", + }, + ["Northwind"] = { + "MERCHANTSHIGHROAD:512:512:256:256", "AMBERSHIRE:512:256:256:256", + "AMBERWOODKEEP:512:256:0:256", "CRYSTALFALLS:512:512:512:256", + "CRAWFORDWINERY:256:256:512:256", "NORTHWINDLOGGINGCAMP:256:512:256:0", + "WITCHCOVEN:256:256:256:0", "RUINSOFBIRKHAVEN:512:512:512:0", + "SHERWOODQUARRY:512:512:512:0", "BLACKROCKBREACH:512:512:512:0", + "GRIMMENLAKE:256:512:512:256", "ABBEYGARDENS:256:256:512:0", + "STILLHEARTPORT:512:512:0:0", "TOWEROFMAGILOU:512:512:0:0", + "BRISTLEWHISKERCAVERN:256:512:512:0", "NORTHRIDGEPOINT:512:512:256:0", + "CINDERFALLPASS:512:512:512:256", + }, +} + +local function IsTurtleWoW() + return TargetHPText and TargetHPPercText +end + +local function GetOverlayDB() + return MapOverlayData or LibMapOverlayData or zMapOverlayData or mapOverlayData +end + +local function PatchOverlayDB() + if overlayDBPatched then return end + overlayDBPatched = true + + if not IsTurtleWoW() then return end + + local db = GetOverlayDB() + if not db then return end + + for zone, data in pairs(TurtleWoW_Zones) do + db[zone] = data + end +end + +local function GetConfig() + if not SFramesDB or type(SFramesDB.MapReveal) ~= "table" then + return { enabled = true, unexploredAlpha = 0.7 } + end + return SFramesDB.MapReveal +end + +local function NextPowerOf2(n) + local p = 16 + while p < n do + p = p * 2 + end + return p +end + +local function DoMapRevealUpdate() + local db = GetOverlayDB() + if not db then return end + + local mapFileName = GetMapInfo and GetMapInfo() + if not mapFileName then mapFileName = "World" end + + local zoneData = db[mapFileName] + if not zoneData then return end + + local prefix = "Interface\\WorldMap\\" .. mapFileName .. "\\" + + local numExploredOverlays = GetNumMapOverlays and GetNumMapOverlays() or 0 + local explored = {} + for i = 1, numExploredOverlays do + local textureName = GetMapOverlayInfo(i) + if textureName and textureName ~= "" then + explored[textureName] = true + end + end + + local cfg = GetConfig() + local dimR, dimG, dimB = 0.4, 0.4, 0.4 + if cfg.unexploredAlpha then + dimR = cfg.unexploredAlpha + dimG = cfg.unexploredAlpha + dimB = cfg.unexploredAlpha + end + + local textureCount = 0 + + for idx = 1, table.getn(zoneData) do + local entry = zoneData[idx] + local _, _, name, sW, sH, sX, sY = string.find(entry, "^(%u+):(%d+):(%d+):(%d+):(%d+)$") + if not name then + _, _, name, sW, sH, sX, sY = string.find(entry, "^([^:]+):(%d+):(%d+):(%d+):(%d+)$") + end + if name then + local textureWidth = tonumber(sW) + local textureHeight = tonumber(sH) + local offsetX = tonumber(sX) + local offsetY = tonumber(sY) + local textureName = prefix .. name + + local isExplored = explored[textureName] + + if cfg.enabled or isExplored then + if errata[textureName] then + local e = errata[textureName] + if e.offsetX and e.offsetX[1] == offsetX then + offsetX = e.offsetX[2] + end + if e.offsetY and e.offsetY[1] == offsetY then + offsetY = e.offsetY[2] + end + end + + local numTexturesHorz = math.ceil(textureWidth / 256) + local numTexturesVert = math.ceil(textureHeight / 256) + local neededTextures = textureCount + (numTexturesHorz * numTexturesVert) + + if neededTextures > NUM_WORLDMAP_OVERLAYS then + for j = NUM_WORLDMAP_OVERLAYS + 1, neededTextures do + WorldMapDetailFrame:CreateTexture("WorldMapOverlay" .. j, "ARTWORK") + end + NUM_WORLDMAP_OVERLAYS = neededTextures + end + + for row = 1, numTexturesVert do + local texturePixelHeight, textureFileHeight + if row < numTexturesVert then + texturePixelHeight = 256 + textureFileHeight = 256 + else + texturePixelHeight = math.mod(textureHeight, 256) + if texturePixelHeight == 0 then texturePixelHeight = 256 end + textureFileHeight = NextPowerOf2(texturePixelHeight) + end + + for col = 1, numTexturesHorz do + if textureCount > NUM_WORLDMAP_OVERLAYS then return end + + local texture = _G["WorldMapOverlay" .. (textureCount + 1)] + + local texturePixelWidth, textureFileWidth + if col < numTexturesHorz then + texturePixelWidth = 256 + textureFileWidth = 256 + else + texturePixelWidth = math.mod(textureWidth, 256) + if texturePixelWidth == 0 then texturePixelWidth = 256 end + textureFileWidth = NextPowerOf2(texturePixelWidth) + end + + texture:SetWidth(texturePixelWidth) + texture:SetHeight(texturePixelHeight) + texture:SetTexCoord(0, texturePixelWidth / textureFileWidth, + 0, texturePixelHeight / textureFileHeight) + texture:ClearAllPoints() + texture:SetPoint("TOPLEFT", "WorldMapDetailFrame", "TOPLEFT", + offsetX + (256 * (col - 1)), + -(offsetY + (256 * (row - 1)))) + + local tileIndex = ((row - 1) * numTexturesHorz) + col + texture:SetTexture(textureName .. tileIndex) + + if not isExplored then + texture:SetVertexColor(dimR, dimG, dimB, 1) + else + texture:SetVertexColor(1, 1, 1, 1) + end + + texture:Show() + textureCount = textureCount + 1 + end + end + end + end + end +end + +function MapReveal:Initialize() + local db = GetOverlayDB() + if not db then + SFrames:Print("MapReveal: LibMapOverlayData 未找到,地图揭示功能不可用。") + return + end + + PatchOverlayDB() + + if not origWorldMapFrame_Update and WorldMapFrame_Update then + origWorldMapFrame_Update = WorldMapFrame_Update + WorldMapFrame_Update = function() + for i = 1, NUM_WORLDMAP_OVERLAYS do + local tex = _G["WorldMapOverlay" .. i] + if tex then tex:Hide() end + end + + origWorldMapFrame_Update() + + local cfg = GetConfig() + if cfg.enabled then + DoMapRevealUpdate() + end + end + end +end + +function MapReveal:Toggle() + if not SFramesDB then SFramesDB = {} end + if type(SFramesDB.MapReveal) ~= "table" then + SFramesDB.MapReveal = { enabled = true, unexploredAlpha = 0.7 } + end + + SFramesDB.MapReveal.enabled = not SFramesDB.MapReveal.enabled + + if SFramesDB.MapReveal.enabled then + SFrames:Print("地图迷雾揭示: |cff00ff00已开启|r") + if not origWorldMapFrame_Update and WorldMapFrame_Update then + self:Initialize() + end + else + SFrames:Print("地图迷雾揭示: |cffff0000已关闭|r") + end + + if WorldMapFrame and WorldMapFrame:IsShown() then + WorldMapFrame_Update() + end +end + +function MapReveal:SetAlpha(alpha) + if not SFramesDB or type(SFramesDB.MapReveal) ~= "table" then return end + SFramesDB.MapReveal.unexploredAlpha = alpha + if SFramesDB.MapReveal.enabled and WorldMapFrame and WorldMapFrame:IsShown() then + WorldMapFrame_Update() + end +end + +function MapReveal:Refresh() + if WorldMapFrame and WorldMapFrame:IsShown() then + WorldMapFrame_Update() + end +end diff --git a/Media.lua b/Media.lua new file mode 100644 index 0000000..d4ef9f5 --- /dev/null +++ b/Media.lua @@ -0,0 +1,47 @@ +SFrames.Media = { + -- Default ElvUI-like texture + -- Default ElvUI-like texture + -- Using the standard flat Vanilla UI status bar texture for a clean flat aesthetic + statusbar = "Interface\\TargetingFrame\\UI-StatusBar", + + -- Fonts + -- We can use a default WoW font, or eventually load a custom one here. + font = "Fonts\\ARIALN.TTF", + fontOutline = "OUTLINE", +} + +function SFrames:GetSharedMedia() + if LibStub then + local ok, LSM = pcall(function() return LibStub("LibSharedMedia-3.0", true) end) + if ok and LSM then return LSM end + end + return nil +end + +function SFrames:GetTexture() + if SFramesDB and SFramesDB.barTexture then + local LSM = self:GetSharedMedia() + if LSM then + local path = LSM:Fetch("statusbar", SFramesDB.barTexture, true) + if path then return path end + end + end + return self.Media.statusbar +end + +function SFrames:GetFont() + if SFramesDB and SFramesDB.fontName then + local LSM = self:GetSharedMedia() + if LSM then + local path = LSM:Fetch("font", SFramesDB.fontName, true) + if path then return path end + end + end + return self.Media.font +end + +function SFrames:GetSharedMediaList(mediaType) + local LSM = self:GetSharedMedia() + if LSM and LSM.List then return LSM:List(mediaType) end + return nil +end diff --git a/Merchant.lua b/Merchant.lua new file mode 100644 index 0000000..29f8738 --- /dev/null +++ b/Merchant.lua @@ -0,0 +1,1110 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Merchant UI (Merchant.lua) +-- Replaces MerchantFrame with Nanami-UI styled interface +-- Tabs: Merchant / Buyback, 4x5 grid, batch buy, repair +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.Merchant = {} +local MUI = SFrames.Merchant +SFramesDB = SFramesDB or {} + +-------------------------------------------------------------------------------- +-- Theme (Pink Cat-Paw) +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + moneyGold = { 1, 0.84, 0.0 }, + moneySilver = { 0.78, 0.78, 0.78 }, + moneyCopper = { 0.71, 0.43, 0.18 }, + cantAfford = { 0.80, 0.20, 0.20 }, + unusable = { 1.00, 0.25, 0.25 }, +}) + +local QUALITY_COLORS = { + [0] = { 0.62, 0.62, 0.62 }, [1] = { 1, 1, 1 }, + [2] = { 0.12, 1, 0 }, [3] = { 0.0, 0.44, 0.87 }, + [4] = { 0.64, 0.21, 0.93 }, [5] = { 1, 0.5, 0 }, +} + +-------------------------------------------------------------------------------- +-- Layout +-------------------------------------------------------------------------------- +local NUM_COLS = 4 +local NUM_ROWS = 5 +local ITEMS_PER_PAGE = NUM_COLS * NUM_ROWS +local FRAME_W = 620 +local HEADER_H = 34 +local SIDE_PAD = 14 +local ITEM_W = 138 +local ITEM_H = 42 +local ITEM_GAP_X = 6 +local ITEM_GAP_Y = 4 +local ICON_SIZE = 34 +local BOTTOM_H = 50 +local TAB_AREA_H = 28 +local FRAME_H = HEADER_H + 6 + TAB_AREA_H + (ITEM_H + ITEM_GAP_Y) * NUM_ROWS + 6 + BOTTOM_H + +-------------------------------------------------------------------------------- +-- State +-------------------------------------------------------------------------------- +local MainFrame = nil +local ItemButtons = {} +local CurrentPage = 1 +local CurrentTab = 1 +local BuyPopup = nil +local ScanTip = nil + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) +end + +local function CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.45) + s:SetBackdropBorderColor(0, 0, 0, 0.6) + s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) + return s +end + +local function FormatMoney(copper) + if not copper or copper <= 0 then return 0, 0, 0 end + local g = math.floor(copper / 10000) + local s = math.floor(math.mod(copper, 10000) / 100) + local c = math.mod(copper, 100) + return g, s, c +end + +local function GetBuybackQualityColor(index) + if not ScanTip then + ScanTip = CreateFrame("GameTooltip", "SFramesMerchantScanTip", UIParent, "GameTooltipTemplate") + ScanTip:SetOwner(UIParent, "ANCHOR_NONE") + end + ScanTip:ClearLines() + ScanTip:SetBuybackItem(index) + local textLine = _G["SFramesMerchantScanTipTextLeft1"] + if textLine then + local r, g, b = textLine:GetTextColor() + if r and g and b then return r, g, b end + end + return nil +end + +-------------------------------------------------------------------------------- +-- Item Stack Helpers +-------------------------------------------------------------------------------- +local function GetMaxStack(link) + if not link then return 1 end + local _, _, _, _, _, _, ms = GetItemInfo(link) + if ms and ms > 0 then return ms end + return 1 +end + +local function GetItemCountInBags(link) + if not link then return 0 end + local _, _, searchID = string.find(link, "item:(%d+)") + if not searchID then return 0 end + local total = 0 + for bag = 0, 4 do + local slots = GetContainerNumSlots(bag) + for slot = 1, slots do + local bagLink = GetContainerItemLink(bag, slot) + if bagLink then + local _, _, bagID = string.find(bagLink, "item:(%d+)") + if bagID == searchID then + local _, count = GetContainerItemInfo(bag, slot) + total = total + (count or 1) + end + end + end + end + return total +end + +-------------------------------------------------------------------------------- +-- Rounded Action Button Factory (for popups) +-------------------------------------------------------------------------------- +local function CreateRoundActionBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 100) + btn:SetHeight(28) + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 11, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text) + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + btn.label = fs + + btn.disabled = false + function btn:SetDisabled(flag) + self.disabled = flag + if flag then + self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) + self:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], 0.5) + else + self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + end + end + + btn:SetScript("OnEnter", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end + end) + btn:SetScript("OnLeave", function() + if not this.disabled then + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end) + btn:SetScript("OnMouseDown", function() + if not this.disabled then + this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) + end + end) + btn:SetScript("OnMouseUp", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + end + end) + + return btn +end + +-------------------------------------------------------------------------------- +-- Action Button Factory (matching TrainerUI / QuestUI) +-------------------------------------------------------------------------------- +local function CreateActionBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 100) + btn:SetHeight(28) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 12, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text) + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + btn.label = fs + + btn.disabled = false + function btn:SetDisabled(flag) + self.disabled = flag + if flag then + self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) + self:SetBackdropBorderColor(0.2, 0.15, 0.18, 0.5) + else + self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + end + end + + btn:SetScript("OnEnter", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end + end) + btn:SetScript("OnLeave", function() + if not this.disabled then + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end) + btn:SetScript("OnMouseDown", function() + if not this.disabled then + this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) + end + end) + btn:SetScript("OnMouseUp", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + end + end) + + return btn +end + +-------------------------------------------------------------------------------- +-- Tab Button Factory +-------------------------------------------------------------------------------- +local function CreateTabBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 70) + btn:SetHeight(22) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + btn:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) + btn:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 12, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text) + fs:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + btn.label = fs + + btn:SetScript("OnEnter", function() + if not this.active then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + end + end) + btn:SetScript("OnLeave", function() + if this.active then + this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) + this:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) + else + this:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) + this:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) + end + end) + + function btn:SetActive(flag) + self.active = flag + if flag then + self:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) + self:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) + self.label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + else + self:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) + self:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) + self.label:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + end + end + + return btn +end + +-------------------------------------------------------------------------------- +-- Buy Popup (Nanami themed, rounded corners, simple quantity input) +-------------------------------------------------------------------------------- +local function EnsureBuyPopup() + if BuyPopup then return end + + BuyPopup = CreateFrame("Frame", "SFramesMerchantBuyPopup", UIParent) + BuyPopup:SetWidth(240) + BuyPopup:SetHeight(110) + BuyPopup:SetPoint("CENTER", UIParent, "CENTER", 0, 60) + BuyPopup:SetFrameStrata("DIALOG") + SetRoundBackdrop(BuyPopup) + CreateShadow(BuyPopup) + BuyPopup:Hide() + + local font = GetFont() + + local titleFS = BuyPopup:CreateFontString(nil, "OVERLAY") + titleFS:SetFont(font, 13, "OUTLINE") + titleFS:SetPoint("TOP", BuyPopup, "TOP", 0, -12) + titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + titleFS:SetText("批量购买") + BuyPopup.titleFS = titleFS + + local customLabel = BuyPopup:CreateFontString(nil, "OVERLAY") + customLabel:SetFont(font, 11, "OUTLINE") + customLabel:SetPoint("TOP", titleFS, "BOTTOM", -18, -10) + customLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + customLabel:SetText("购买数量:") + + local editbox = CreateFrame("EditBox", "SFramesMerchantBuyEditBox", BuyPopup) + editbox:SetWidth(60) + editbox:SetHeight(22) + editbox:SetPoint("LEFT", customLabel, "RIGHT", 6, 0) + editbox:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + editbox:SetBackdropColor(T.inputBg[1], T.inputBg[2], T.inputBg[3], T.inputBg[4] or 0.95) + editbox:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], 0.8) + editbox:SetFont(font, 12, "OUTLINE") + editbox:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + editbox:SetJustifyH("CENTER") + editbox:SetNumeric(true) + editbox:SetAutoFocus(false) + editbox:SetTextInsets(4, 4, 2, 2) + + local confirm = CreateRoundActionBtn(BuyPopup, "确定", 75) + confirm:SetPoint("BOTTOMLEFT", BuyPopup, "BOTTOMLEFT", 28, 12) + + local cancel = CreateRoundActionBtn(BuyPopup, "取消", 75) + cancel:SetPoint("BOTTOMRIGHT", BuyPopup, "BOTTOMRIGHT", -28, 12) + + BuyPopup.index = 1 + BuyPopup.maxPurchase = 100 + BuyPopup.itemLink = nil + + local function DoBuy() + local qty = tonumber(editbox:GetText()) or 0 + if qty > 0 then + if qty > BuyPopup.maxPurchase then qty = BuyPopup.maxPurchase end + MUI:BuyMultiple(BuyPopup.index, qty) + end + BuyPopup:Hide() + end + + confirm:SetScript("OnClick", DoBuy) + cancel:SetScript("OnClick", function() BuyPopup:Hide() end) + editbox:SetScript("OnEnterPressed", DoBuy) + editbox:SetScript("OnEscapePressed", function() BuyPopup:Hide() end) + + BuyPopup.ShowPopup = function(index, maxAfford, itemLink) + BuyPopup.index = index + BuyPopup.maxPurchase = math.min(9999, maxAfford) + BuyPopup.itemLink = itemLink + editbox:SetText("1") + + BuyPopup.titleFS:SetText("批量购买") + + BuyPopup:Show() + editbox:SetFocus() + end +end + +-------------------------------------------------------------------------------- +-- Batch Buy Logic +-------------------------------------------------------------------------------- +function MUI:BuyMultiple(index, totalAmount) + if totalAmount <= 0 then return end + if CurrentTab == 2 then + BuybackItem(index) + return + end + + local name, _, price, batchQty, numAvailable = GetMerchantItemInfo(index) + + if numAvailable > -1 and totalAmount > numAvailable then + totalAmount = numAvailable + end + + for i = 1, totalAmount do + BuyMerchantItem(index, 1) + end +end + +-------------------------------------------------------------------------------- +-- Merchant Item Button Factory +-------------------------------------------------------------------------------- +local function CreateMerchantButton(parent, id) + local btn = CreateFrame("Button", "SFramesMerchantItem" .. id, parent) + btn:SetWidth(ITEM_W) + btn:SetHeight(ITEM_H) + + local iconFrame = CreateFrame("Button", btn:GetName() .. "Icon", btn) + iconFrame:SetWidth(ICON_SIZE) + iconFrame:SetHeight(ICON_SIZE) + iconFrame:SetPoint("LEFT", btn, "LEFT", 2, 0) + iconFrame:RegisterForClicks("LeftButtonUp", "RightButtonUp") + iconFrame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + btn.iconFrame = iconFrame + + local qualGlow = iconFrame:CreateTexture(nil, "OVERLAY") + qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + qualGlow:SetBlendMode("ADD") + qualGlow:SetAlpha(0.7) + qualGlow:SetWidth(ICON_SIZE * 1.8) + qualGlow:SetHeight(ICON_SIZE * 1.8) + qualGlow:SetPoint("CENTER", iconFrame, "CENTER", 0, 0) + qualGlow:Hide() + btn.qualGlow = qualGlow + + function btn:SetQualityBorder(r, g, b) + self.qualGlow:SetVertexColor(r, g, b) + self.qualGlow:Show() + self.iconFrame:SetBackdropBorderColor(r, g, b, 1) + end + function btn:ResetBorder() + self.qualGlow:Hide() + self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + end + + local icon = iconFrame:CreateTexture(nil, "ARTWORK") + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 3, -3) + icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -3, 3) + btn.icon = icon + + local font = GetFont() + local nameFS = btn:CreateFontString(nil, "OVERLAY") + nameFS:SetFont(font, 11, "OUTLINE") + nameFS:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 5, -1) + nameFS:SetPoint("RIGHT", btn, "RIGHT", -2, 0) + nameFS:SetJustifyH("LEFT") + nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + btn.nameFS = nameFS + + btn.gTxt = btn:CreateFontString(nil, "OVERLAY") + btn.gTxt:SetFont(font, 10, "OUTLINE") + btn.gTex = btn:CreateTexture(nil, "ARTWORK") + btn.gTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") + btn.gTex:SetTexCoord(0, 0.25, 0, 1) + btn.gTex:SetWidth(11); btn.gTex:SetHeight(11) + + btn.sTxt = btn:CreateFontString(nil, "OVERLAY") + btn.sTxt:SetFont(font, 10, "OUTLINE") + btn.sTex = btn:CreateTexture(nil, "ARTWORK") + btn.sTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") + btn.sTex:SetTexCoord(0.25, 0.5, 0, 1) + btn.sTex:SetWidth(11); btn.sTex:SetHeight(11) + + btn.cTxt = btn:CreateFontString(nil, "OVERLAY") + btn.cTxt:SetFont(font, 10, "OUTLINE") + btn.cTex = btn:CreateTexture(nil, "ARTWORK") + btn.cTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") + btn.cTex:SetTexCoord(0.5, 0.75, 0, 1) + btn.cTex:SetWidth(11); btn.cTex:SetHeight(11) + + local countFS = iconFrame:CreateFontString(nil, "OVERLAY") + countFS:SetFont("Fonts\\ARIALN.TTF", 11, "OUTLINE") + countFS:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -2, 2) + countFS:SetJustifyH("RIGHT") + btn.countFS = countFS + + local highlight = btn:CreateTexture(nil, "HIGHLIGHT") + highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + highlight:SetBlendMode("ADD") + highlight:SetAllPoints(btn) + highlight:SetAlpha(0.15) + + local function OnEnter() + if not btn.itemIndex then return end + btn.iconFrame:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4]) + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + if CurrentTab == 1 then + GameTooltip:SetMerchantItem(btn.itemIndex) + ShowMerchantSellCursor(btn.itemIndex) + local _, _, price = GetMerchantItemInfo(btn.itemIndex) + if price and price > 0 then + GameTooltip:AddLine(" ") + GameTooltip:AddLine(">> 本店售价:", 1, 0.8, 0) + SetTooltipMoney(GameTooltip, price) + end + else + GameTooltip:SetBuybackItem(btn.itemIndex) + local _, _, price = GetBuybackItemInfo(btn.itemIndex) + if price and price > 0 then + GameTooltip:AddLine(" ") + GameTooltip:AddLine(">> 购回需花费:", 1, 0.5, 0) + SetTooltipMoney(GameTooltip, price) + end + end + local ttLink = (CurrentTab == 1) and GetMerchantItemLink(btn.itemIndex) or nil + if not ttLink and CurrentTab == 2 and GetBuybackItemLink then + ttLink = GetBuybackItemLink(btn.itemIndex) + end + if ttLink then + local ms = GetMaxStack(ttLink) + if ms and ms > 1 then + GameTooltip:AddLine(" ") + GameTooltip:AddLine("最大堆叠: " .. ms, 0.5, 0.8, 1) + end + end + if IsControlKeyDown() then ShowInspectCursor() end + GameTooltip:Show() + end + + local function OnLeave() + GameTooltip:Hide() + ResetCursor() + if btn._qualR then + btn.iconFrame:SetBackdropBorderColor(btn._qualR, btn._qualG, btn._qualB, 1) + else + btn.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + end + end + + iconFrame:SetScript("OnEnter", OnEnter) + iconFrame:SetScript("OnLeave", OnLeave) + + iconFrame:SetScript("OnUpdate", function() + if GameTooltip:IsOwned(this) then + if IsControlKeyDown() then + if not this.controlDownLast then + this.controlDownLast = true + ShowInspectCursor() + end + else + if this.controlDownLast then + this.controlDownLast = false + ResetCursor() + end + end + end + end) + + iconFrame:SetScript("OnClick", function() + if not btn.itemIndex then return end + if IsControlKeyDown() and arg1 == "LeftButton" then + local link = GetMerchantItemLink(btn.itemIndex) + if link and DressUpItemLink then + DressUpItemLink(link) + return + end + end + if IsShiftKeyDown() then + if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then + local link = nil + if CurrentTab == 1 then link = GetMerchantItemLink(btn.itemIndex) end + if link then ChatFrameEditBox:Insert(link) end + else + if CurrentTab == 1 then + local name, _, price, quantity, numAvailable = GetMerchantItemInfo(btn.itemIndex) + if not name then return end + local popupLink = GetMerchantItemLink(btn.itemIndex) + local maxCanAfford = 9999 + if price and price > 0 then + maxCanAfford = math.floor(GetMoney() / price) + end + if numAvailable > -1 and numAvailable < maxCanAfford then + maxCanAfford = numAvailable + end + if maxCanAfford < 1 then return end + EnsureBuyPopup() + BuyPopup.ShowPopup(btn.itemIndex, maxCanAfford, popupLink) + else + BuybackItem(btn.itemIndex) + end + end + elseif arg1 == "RightButton" then + if CurrentTab == 1 then + BuyMerchantItem(btn.itemIndex, 1) + else + BuybackItem(btn.itemIndex) + end + end + end) + + return btn +end + +-------------------------------------------------------------------------------- +-- Money Layout Helper +-------------------------------------------------------------------------------- +local function LayoutMoney(btn, copper, canBuy) + btn.gTxt:Hide(); btn.gTex:Hide() + btn.sTxt:Hide(); btn.sTex:Hide() + btn.cTxt:Hide(); btn.cTex:Hide() + + local r, g, b + if canBuy then + r, g, b = T.bodyText[1], T.bodyText[2], T.bodyText[3] + else + r, g, b = T.cantAfford[1], T.cantAfford[2], T.cantAfford[3] + end + + if copper == 0 then + btn.cTxt:SetText("0") + btn.cTxt:SetTextColor(r, g, b) + btn.cTxt:ClearAllPoints() + btn.cTxt:SetPoint("BOTTOMLEFT", btn.iconFrame, "BOTTOMRIGHT", 5, 2) + btn.cTxt:Show() + btn.cTex:ClearAllPoints() + btn.cTex:SetPoint("LEFT", btn.cTxt, "RIGHT", 1, 0) + btn.cTex:Show() + return + end + + local vG, vS, vC = FormatMoney(copper) + local anchor = nil + + local function AttachPair(txt, tex, val, coinR, coinG, coinB) + txt:SetText(val) + txt:SetTextColor(canBuy and coinR or r, canBuy and coinG or g, canBuy and coinB or b) + txt:ClearAllPoints() + if not anchor then + txt:SetPoint("BOTTOMLEFT", btn.iconFrame, "BOTTOMRIGHT", 5, 2) + else + txt:SetPoint("LEFT", anchor, "RIGHT", 4, 0) + end + txt:Show() + tex:ClearAllPoints() + tex:SetPoint("LEFT", txt, "RIGHT", 1, 0) + tex:Show() + anchor = tex + end + + if vG > 0 then AttachPair(btn.gTxt, btn.gTex, vG, T.moneyGold[1], T.moneyGold[2], T.moneyGold[3]) end + if vS > 0 then AttachPair(btn.sTxt, btn.sTex, vS, T.moneySilver[1], T.moneySilver[2], T.moneySilver[3]) end + if vC > 0 then AttachPair(btn.cTxt, btn.cTex, vC, T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3]) end +end + +-------------------------------------------------------------------------------- +-- Update +-------------------------------------------------------------------------------- +function MUI:Update() + if not MainFrame or not MainFrame:IsVisible() then return end + + local numItems = (CurrentTab == 1) and GetMerchantNumItems() or GetNumBuybackItems() + local name = UnitName("NPC") or "商人" + if CurrentTab == 2 then name = name .. " - 购回" end + MainFrame.npcNameFS:SetText(name) + + local totalPages = math.max(1, math.ceil(numItems / ITEMS_PER_PAGE)) + if CurrentPage > totalPages then CurrentPage = totalPages end + if CurrentPage < 1 then CurrentPage = 1 end + + MainFrame.pageText:SetText(string.format("第 %d / %d 页", CurrentPage, totalPages)) + MainFrame.prevBtn:SetDisabled(CurrentPage <= 1) + MainFrame.nextBtn:SetDisabled(CurrentPage >= totalPages) + + MainFrame.tabMerchant:SetActive(CurrentTab == 1) + MainFrame.tabBuyback:SetActive(CurrentTab == 2) + + for i = 1, ITEMS_PER_PAGE do + local btn = ItemButtons[i] + local itemIndex = (CurrentPage - 1) * ITEMS_PER_PAGE + i + + if itemIndex <= numItems then + local itemName, itemTexture, itemPrice, itemQuantity, numAvailable, isUsable + local itemLink + + if CurrentTab == 1 then + itemName, itemTexture, itemPrice, itemQuantity, numAvailable, isUsable = GetMerchantItemInfo(itemIndex) + itemLink = GetMerchantItemLink(itemIndex) + else + itemName, itemTexture, itemPrice, itemQuantity, numAvailable, isUsable = GetBuybackItemInfo(itemIndex) + if GetBuybackItemLink then itemLink = GetBuybackItemLink(itemIndex) end + end + + btn.itemIndex = itemIndex + btn.icon:SetTexture(itemTexture) + btn.nameFS:SetText(itemName) + btn._qualR = nil; btn._qualG = nil; btn._qualB = nil + + if CurrentTab == 2 and isUsable == nil then isUsable = true end + local canAfford = (GetMoney() >= itemPrice) + local isRequirementBlocked = (CurrentTab == 1 and not isUsable) + local canBuy = (not isRequirementBlocked) and canAfford + if canBuy then + btn.icon:SetVertexColor(1, 1, 1) + btn.nameFS:SetAlpha(1) + else + btn.icon:SetVertexColor(0.5, 0.2, 0.2) + btn.nameFS:SetAlpha(0.6) + end + + LayoutMoney(btn, itemPrice, canBuy) + + if CurrentTab == 1 and numAvailable and numAvailable > -1 then + btn.countFS:SetText(numAvailable) + if numAvailable == 0 then + btn.countFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + else + btn.countFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + end + elseif itemQuantity and itemQuantity > 1 then + btn.countFS:SetText(itemQuantity) + btn.countFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + else + btn.countFS:SetText("") + end + + local qr, qg, qb + local quality + + if itemLink then + local _, _, q = GetItemInfo(itemLink) + quality = q + if not quality then + local _, _, itemID = string.find(itemLink, "item:(%d+)") + if itemID then _, _, quality = GetItemInfo("item:" .. itemID) end + end + if quality then + qr, qg, qb = GetItemQualityColor(quality) + else + local _, _, hexColor = string.find(itemLink, "|c(%x%x%x%x%x%x%x%x)|") + if hexColor then + qr = tonumber(string.sub(hexColor, 3, 4), 16) / 255 + qg = tonumber(string.sub(hexColor, 5, 6), 16) / 255 + qb = tonumber(string.sub(hexColor, 7, 8), 16) / 255 + end + end + elseif CurrentTab == 2 and itemName then + qr, qg, qb = GetBuybackQualityColor(itemIndex) + end + + if isRequirementBlocked then + -- Level / weapon skill / class requirements not met: always mark red. + btn.nameFS:SetTextColor(T.unusable[1], T.unusable[2], T.unusable[3]) + btn.nameFS:SetAlpha(1) + btn.icon:SetVertexColor(0.75, 0.25, 0.25) + btn:SetQualityBorder(T.unusable[1], T.unusable[2], T.unusable[3]) + btn._qualR = T.unusable[1]; btn._qualG = T.unusable[2]; btn._qualB = T.unusable[3] + elseif qr then + btn.nameFS:SetTextColor(qr, qg, qb) + btn._qualR = qr; btn._qualG = qg; btn._qualB = qb + if quality and quality > 1 then + btn:SetQualityBorder(qr, qg, qb) + else + local isWhite = (qr > 0.95 and qg > 0.95 and qb > 0.95) + local isGrey = (qr < 0.7 and qg < 0.7 and qb < 0.7) + if not isWhite and not isGrey then + btn:SetQualityBorder(qr, qg, qb) + else + btn:ResetBorder() + btn._qualR = nil; btn._qualG = nil; btn._qualB = nil + end + end + else + btn.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + btn:ResetBorder() + end + + if CurrentTab == 1 and numAvailable and numAvailable == 0 then + btn.icon:SetVertexColor(0.4, 0.4, 0.4) + btn.nameFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + btn.nameFS:SetAlpha(0.5) + btn:ResetBorder() + btn._qualR = nil; btn._qualG = nil; btn._qualB = nil + LayoutMoney(btn, itemPrice, false) + end + + btn:Show() + else + btn.itemIndex = nil + btn._qualR = nil; btn._qualG = nil; btn._qualB = nil + btn:ResetBorder() + btn:Hide() + end + end + + if CanMerchantRepair() then + MainFrame.repairAllBtn:Show() + MainFrame.repairItemBtn:Show() + local repairCost, canRepair = GetRepairAllCost() + if canRepair and repairCost > 0 and (GetMoney() >= repairCost) then + MainFrame.repairAllBtn.canClick = true + if MainFrame.repairAllBtn.iconTex then + MainFrame.repairAllBtn.iconTex:SetVertexColor(1, 1, 1) + end + else + MainFrame.repairAllBtn.canClick = false + if MainFrame.repairAllBtn.iconTex then + MainFrame.repairAllBtn.iconTex:SetVertexColor(0.5, 0.5, 0.5) + end + end + else + MainFrame.repairAllBtn:Hide() + MainFrame.repairItemBtn:Hide() + end +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function MUI:Initialize() + if MainFrame then return end + + MainFrame = CreateFrame("Frame", "SFramesMerchantFrame", UIParent) + MainFrame:SetWidth(FRAME_W) + MainFrame:SetHeight(FRAME_H) + MainFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 12, -104) + MainFrame:SetFrameStrata("HIGH") + MainFrame:SetToplevel(true) + MainFrame:EnableMouse(true) + MainFrame:SetMovable(true) + MainFrame:RegisterForDrag("LeftButton") + MainFrame:SetScript("OnDragStart", function() this:StartMoving() end) + MainFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + SetRoundBackdrop(MainFrame) + CreateShadow(MainFrame) + + -- Header + local header = CreateFrame("Frame", nil, MainFrame) + header:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 0, 0) + header:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 0, 0) + header:SetHeight(HEADER_H) + header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) + header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) + + local titleIco = SFrames:CreateIcon(header, "merchant", 16) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0) + titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) + + local npcNameFS = header:CreateFontString(nil, "OVERLAY") + npcNameFS:SetFont(GetFont(), 14, "OUTLINE") + npcNameFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) + npcNameFS:SetPoint("RIGHT", header, "RIGHT", -30, 0) + npcNameFS:SetJustifyH("LEFT") + npcNameFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + MainFrame.npcNameFS = npcNameFS + + local closeBtn = CreateFrame("Button", nil, header) + closeBtn:SetWidth(20); closeBtn:SetHeight(20) + closeBtn:SetPoint("RIGHT", header, "RIGHT", -8, 0) + local closeTex = closeBtn:CreateTexture(nil, "ARTWORK") + closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") + closeTex:SetTexCoord(0.25, 0.375, 0, 0.125) + closeTex:SetAllPoints() + closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) + closeBtn:SetScript("OnClick", function() MainFrame:Hide() end) + closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end) + closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) + + local headerSep = MainFrame:CreateTexture(nil, "ARTWORK") + headerSep:SetTexture("Interface\\Buttons\\WHITE8X8") + headerSep:SetHeight(1) + headerSep:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 6, -HEADER_H) + headerSep:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -6, -HEADER_H) + headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + -- Tabs + local tabMerchant = CreateTabBtn(MainFrame, "商人", 60) + tabMerchant:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -(HEADER_H + 6)) + tabMerchant:SetScript("OnClick", function() + CurrentTab = 1 + CurrentPage = 1 + MUI:Update() + end) + MainFrame.tabMerchant = tabMerchant + + local tabBuyback = CreateTabBtn(MainFrame, "购回", 60) + tabBuyback:SetPoint("LEFT", tabMerchant, "RIGHT", 4, 0) + tabBuyback:SetScript("OnClick", function() + CurrentTab = 2 + CurrentPage = 1 + MUI:Update() + end) + MainFrame.tabBuyback = tabBuyback + + -- Item Grid + local gridTop = HEADER_H + 6 + 22 + 6 + for i = 1, ITEMS_PER_PAGE do + local btn = CreateMerchantButton(MainFrame, i) + local row = math.floor((i - 1) / NUM_COLS) + local col = math.mod((i - 1), NUM_COLS) + btn:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", + SIDE_PAD + col * (ITEM_W + ITEM_GAP_X), + -(gridTop + row * (ITEM_H + ITEM_GAP_Y))) + ItemButtons[i] = btn + end + + -- Bottom bar separator + local bottomSep = MainFrame:CreateTexture(nil, "ARTWORK") + bottomSep:SetTexture("Interface\\Buttons\\WHITE8X8") + bottomSep:SetHeight(1) + bottomSep:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", 6, BOTTOM_H) + bottomSep:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -6, BOTTOM_H) + bottomSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + -- Page Navigation + local prevBtn = CreateActionBtn(MainFrame, "<", 28) + prevBtn:SetHeight(22) + prevBtn:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, 14) + prevBtn:SetScript("OnClick", function() + CurrentPage = CurrentPage - 1 + MUI:Update() + end) + MainFrame.prevBtn = prevBtn + + local nextBtn = CreateActionBtn(MainFrame, ">", 28) + nextBtn:SetHeight(22) + nextBtn:SetPoint("LEFT", prevBtn, "RIGHT", 4, 0) + nextBtn:SetScript("OnClick", function() + CurrentPage = CurrentPage + 1 + MUI:Update() + end) + MainFrame.nextBtn = nextBtn + + local pageText = MainFrame:CreateFontString(nil, "OVERLAY") + pageText:SetFont(GetFont(), 11, "OUTLINE") + pageText:SetPoint("LEFT", nextBtn, "RIGHT", 8, 0) + pageText:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + MainFrame.pageText = pageText + + -- Repair Buttons + local repairAllBtn = CreateFrame("Button", nil, MainFrame) + repairAllBtn:SetWidth(32); repairAllBtn:SetHeight(32) + repairAllBtn:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -(SIDE_PAD), 10) + repairAllBtn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + repairAllBtn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + repairAllBtn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.6) + + local repairAllIcon = repairAllBtn:CreateTexture(nil, "ARTWORK") + repairAllIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + repairAllIcon:SetPoint("TOPLEFT", 3, -3) + repairAllIcon:SetPoint("BOTTOMRIGHT", -3, 3) + repairAllIcon:SetTexture("Interface\\Icons\\Trade_BlackSmithing") + repairAllBtn.iconTex = repairAllIcon + + repairAllBtn:SetScript("OnClick", function() + if this.canClick then RepairAllItems() end + end) + repairAllBtn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText("修理所有物品", 1, 1, 1) + local repairCost, canRepair = GetRepairAllCost() + if canRepair and repairCost > 0 then SetTooltipMoney(GameTooltip, repairCost) end + GameTooltip:Show() + end) + repairAllBtn:SetScript("OnLeave", function() + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.6) + GameTooltip:Hide() + end) + MainFrame.repairAllBtn = repairAllBtn + + local repairItemBtn = CreateFrame("Button", nil, MainFrame) + repairItemBtn:SetWidth(32); repairItemBtn:SetHeight(32) + repairItemBtn:SetPoint("RIGHT", repairAllBtn, "LEFT", -6, 0) + repairItemBtn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + repairItemBtn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + repairItemBtn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.6) + + local repairItemIcon = repairItemBtn:CreateTexture(nil, "ARTWORK") + repairItemIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + repairItemIcon:SetPoint("TOPLEFT", 3, -3) + repairItemIcon:SetPoint("BOTTOMRIGHT", -3, 3) + repairItemIcon:SetTexture("Interface\\Icons\\INV_Hammer_20") + repairItemBtn.iconTex = repairItemIcon + + repairItemBtn:SetScript("OnClick", function() + if InRepairMode() then HideRepairCursor() else ShowRepairCursor() end + end) + repairItemBtn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText("修理一件物品", 1, 1, 1) + GameTooltip:Show() + end) + repairItemBtn:SetScript("OnLeave", function() + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.6) + GameTooltip:Hide() + end) + MainFrame.repairItemBtn = repairItemBtn + + -- Events + MainFrame:EnableMouseWheel(true) + MainFrame:SetScript("OnMouseWheel", function() + local numItems = (CurrentTab == 1) and GetMerchantNumItems() or GetNumBuybackItems() + local totalPages = math.max(1, math.ceil(numItems / ITEMS_PER_PAGE)) + if arg1 > 0 then + if CurrentPage > 1 then + CurrentPage = CurrentPage - 1 + MUI:Update() + end + elseif arg1 < 0 then + if CurrentPage < totalPages then + CurrentPage = CurrentPage + 1 + MUI:Update() + end + end + end) + + MainFrame:SetScript("OnHide", function() pcall(CloseMerchant) end) + MainFrame:SetScript("OnEvent", function() + if event == "MERCHANT_SHOW" then + if SFramesDB and SFramesDB.enableMerchant == false then return end + CurrentTab = 1 + CurrentPage = 1 + MainFrame:Show() + MUI:Update() + elseif event == "MERCHANT_UPDATE" then + if MainFrame:IsVisible() then MUI:Update() end + elseif event == "MERCHANT_CLOSED" then + MainFrame:Hide() + end + end) + + MainFrame:RegisterEvent("MERCHANT_SHOW") + MainFrame:RegisterEvent("MERCHANT_UPDATE") + MainFrame:RegisterEvent("MERCHANT_CLOSED") + MainFrame:Hide() + + if MerchantFrame then + MerchantFrame:UnregisterEvent("MERCHANT_SHOW") + end + tinsert(UISpecialFrames, "SFramesMerchantFrame") +end + +-------------------------------------------------------------------------------- +-- Bootstrap +-------------------------------------------------------------------------------- +local f = CreateFrame("Frame") +f:RegisterEvent("PLAYER_LOGIN") +f:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + if SFramesDB and SFramesDB.enableMerchant == nil then + SFramesDB.enableMerchant = true + end + if SFramesDB and SFramesDB.enableMerchant ~= false then + MUI:Initialize() + end + end +end) diff --git a/Minimap.lua b/Minimap.lua new file mode 100644 index 0000000..6462412 --- /dev/null +++ b/Minimap.lua @@ -0,0 +1,590 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Minimap Skin (Minimap.lua) +-- Custom cat-paw pixel art frame for the minimap +-------------------------------------------------------------------------------- + +SFrames.Minimap = SFrames.Minimap or {} +local MM = SFrames.Minimap +local _A = SFrames.ActiveTheme + +local MAP_STYLES = { + { key = "map", tex = "Interface\\AddOns\\Nanami-UI\\img\\map", label = "猫爪", plateY = -10, textColor = {0.45, 0.32, 0.20} }, + { key = "zs", tex = "Interface\\AddOns\\Nanami-UI\\img\\zs", label = "战士", plateY = -6, textColor = {1, 1, 1} }, + { key = "qs", tex = "Interface\\AddOns\\Nanami-UI\\img\\qs", label = "圣骑士", plateY = -6, textColor = {0.22, 0.13, 0.07} }, + { key = "lr", tex = "Interface\\AddOns\\Nanami-UI\\img\\lr", label = "猎人", plateY = -6, textColor = {1, 1, 1} }, + { key = "qxz", tex = "Interface\\AddOns\\Nanami-UI\\img\\qxz", label = "潜行者", plateY = -6, textColor = {1, 1, 1} }, + { key = "ms", tex = "Interface\\AddOns\\Nanami-UI\\img\\ms", label = "牧师", plateY = -6, textColor = {0.22, 0.13, 0.07} }, + { key = "sm", tex = "Interface\\AddOns\\Nanami-UI\\img\\sm", label = "萨满", plateY = -6, textColor = {1, 1, 1} }, + { key = "fs", tex = "Interface\\AddOns\\Nanami-UI\\img\\fs", label = "法师", plateY = -6, textColor = {1, 1, 1} }, + { key = "ss", tex = "Interface\\AddOns\\Nanami-UI\\img\\ss", label = "术士", plateY = -6, textColor = {1, 1, 1} }, + { key = "dly", tex = "Interface\\AddOns\\Nanami-UI\\img\\dly", label = "德鲁伊", plateY = -6, textColor = {0.22, 0.13, 0.07} }, +} +local TEX_SIZE = 512 +local CIRCLE_CX = 256 +local CIRCLE_CY = 256 +local CIRCLE_R = 210 +local PLATE_X = 103 +local PLATE_Y = 29 +local PLATE_W = 200 +local PLATE_H = 66 +local BASE_SIZE = 180 + +local CLASS_STYLE_MAP = { + ["Warrior"] = "zs", ["WARRIOR"] = "zs", ["战士"] = "zs", + ["Paladin"] = "qs", ["PALADIN"] = "qs", ["圣骑士"] = "qs", + ["Hunter"] = "lr", ["HUNTER"] = "lr", ["猎人"] = "lr", + ["Rogue"] = "qxz", ["ROGUE"] = "qxz", ["潜行者"] = "qxz", + ["Priest"] = "ms", ["PRIEST"] = "ms", ["牧师"] = "ms", + ["Shaman"] = "sm", ["SHAMAN"] = "sm", ["萨满祭司"] = "sm", + ["Mage"] = "fs", ["MAGE"] = "fs", ["法师"] = "fs", + ["Warlock"] = "ss", ["WARLOCK"] = "ss", ["术士"] = "ss", + ["Druid"] = "dly", ["DRUID"] = "dly", ["德鲁伊"] = "dly", +} + +local DEFAULTS = { + enabled = true, + scale = 1.0, + showClock = true, + showCoords = true, + mapStyle = "auto", + posX = -5, + posY = -5, + mailIconX = nil, + mailIconY = nil, +} + +local container, overlayFrame, overlayTex +local zoneFs, clockFs, clockBg, coordFs +local built = false + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetDB() + if not SFramesDB then SFramesDB = {} end + if type(SFramesDB.Minimap) ~= "table" then SFramesDB.Minimap = {} end + local db = SFramesDB.Minimap + for k, v in pairs(DEFAULTS) do + if db[k] == nil then db[k] = v end + end + return db +end + +local function ResolveStyleKey() + local key = GetDB().mapStyle or "auto" + if key == "auto" then + local localName, engName = UnitClass("player") + key = CLASS_STYLE_MAP[engName] + or CLASS_STYLE_MAP[localName] + or "map" + end + return key +end + +local function GetCurrentStyle() + local key = ResolveStyleKey() + for _, s in ipairs(MAP_STYLES) do + if s.key == key then return s end + end + return MAP_STYLES[1] +end + +local function GetMapTexture() + return GetCurrentStyle().tex +end + +local function S(texPx, frameSize) + return texPx / TEX_SIZE * frameSize +end + +local function FrameSize() + return math.floor(BASE_SIZE * ((GetDB().scale) or 1)) +end + +local function ApplyPosition() + if not container then return end + local db = GetDB() + local x = tonumber(db.posX) or -5 + local y = tonumber(db.posY) or -5 + container:ClearAllPoints() + container:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", x, y) +end + +-------------------------------------------------------------------------------- +-- Hide default Blizzard minimap chrome +-- MUST be called AFTER BuildFrame (Minimap is already reparented) +-------------------------------------------------------------------------------- +local function HideDefaultElements() + local kill = { + MinimapBorder, + MinimapBorderTop, + MinimapZoomIn, + MinimapZoomOut, + MinimapToggleButton, + MiniMapWorldMapButton, + GameTimeFrame, + MinimapZoneTextButton, + MiniMapTracking, + MinimapBackdrop, + } + for _, f in ipairs(kill) do + if f then + f:Hide() + f.Show = function() end + end + end + + -- Hide all tracking-related frames (Turtle WoW dual tracking, etc.) + local trackNames = { + "MiniMapTrackingButton", "MiniMapTrackingFrame", + "MiniMapTrackingIcon", "MiniMapTracking1", "MiniMapTracking2", + } + for _, name in ipairs(trackNames) do + local f = _G[name] + if f and f.Hide then + f:Hide() + f.Show = function() end + end + end + + -- Also hide any tracking textures that are children of Minimap + if Minimap.GetChildren then + local children = { Minimap:GetChildren() } + for _, child in ipairs(children) do + local n = child.GetName and child:GetName() + if n and string.find(n, "Track") then + child:Hide() + child.Show = function() end + end + end + end + + -- Move MinimapCluster off-screen instead of Hide() + -- Hide() would cascade-hide children that were still parented at load time + if MinimapCluster then + MinimapCluster:ClearAllPoints() + MinimapCluster:SetPoint("TOP", UIParent, "BOTTOM", 0, -500) + MinimapCluster:SetWidth(1) + MinimapCluster:SetHeight(1) + MinimapCluster:EnableMouse(false) + end +end + +-------------------------------------------------------------------------------- +-- Build +-------------------------------------------------------------------------------- +local function BuildFrame() + if built then return end + built = true + + local fs = FrameSize() + local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs)) + + -- Main container + container = CreateFrame("Frame", "SFramesMinimapContainer", UIParent) + container:SetWidth(fs) + container:SetHeight(fs) + container:SetFrameStrata("LOW") + container:SetFrameLevel(1) + container:EnableMouse(false) + container:SetClampedToScreen(true) + + -- Reparent the actual minimap into our container + Minimap:SetParent(container) + Minimap:ClearAllPoints() + Minimap:SetPoint("CENTER", container, "TOPLEFT", + S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs)) + Minimap:SetWidth(mapDiam) + Minimap:SetHeight(mapDiam) + Minimap:SetFrameStrata("LOW") + Minimap:SetFrameLevel(2) + Minimap:Show() + + if Minimap.SetMaskTexture then + Minimap:SetMaskTexture("Textures\\MinimapMask") + end + + -- Decorative overlay (map.tga with transparent circle) + overlayFrame = CreateFrame("Frame", nil, container) + overlayFrame:SetAllPoints(container) + overlayFrame:SetFrameStrata("LOW") + overlayFrame:SetFrameLevel(Minimap:GetFrameLevel() + 3) + overlayFrame:EnableMouse(false) + + overlayTex = overlayFrame:CreateTexture(nil, "ARTWORK") + overlayTex:SetTexture(GetMapTexture()) + overlayTex:SetAllPoints(overlayFrame) + + -- Zone name on the scroll plate (horizontally centered on frame) + local style = GetCurrentStyle() + local pcy = S(PLATE_Y + PLATE_H / 2 + (style.plateY or -6), fs) + + zoneFs = overlayFrame:CreateFontString(nil, "OVERLAY") + zoneFs:SetFont(SFrames:GetFont(), 11, "") + zoneFs:SetPoint("CENTER", container, "TOP", 0, -pcy) + zoneFs:SetWidth(S(PLATE_W + 60, fs)) + zoneFs:SetHeight(S(PLATE_H, fs)) + zoneFs:SetJustifyH("CENTER") + zoneFs:SetJustifyV("MIDDLE") + local tc = style.textColor or {0.22, 0.13, 0.07} + zoneFs:SetTextColor(tc[1], tc[2], tc[3]) + + -- Clock background (semi-transparent rounded) + clockBg = CreateFrame("Frame", nil, overlayFrame) + clockBg:SetFrameLevel(overlayFrame:GetFrameLevel() + 1) + clockBg:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + local _clkBg = _A.clockBg or { 0, 0, 0, 0.55 } + local _clkBd = _A.clockBorder or { 0, 0, 0, 0.3 } + clockBg:SetBackdropColor(_clkBg[1], _clkBg[2], _clkBg[3], _clkBg[4]) + clockBg:SetBackdropBorderColor(_clkBd[1], _clkBd[2], _clkBd[3], _clkBd[4]) + clockBg:SetWidth(46) + clockBg:SetHeight(18) + clockBg:SetPoint("CENTER", container, "BOTTOM", 0, + S((TEX_SIZE - CIRCLE_CY - CIRCLE_R) / 2, fs)) + + -- Clock text + clockFs = clockBg:CreateFontString(nil, "OVERLAY") + clockFs:SetFont(SFrames:GetFont(), 10, "OUTLINE") + clockFs:SetPoint("CENTER", clockBg, "CENTER", 0, 0) + clockFs:SetJustifyH("CENTER") + local _clkTxt = _A.clockText or { 0.92, 0.84, 0.72 } + clockFs:SetTextColor(_clkTxt[1], _clkTxt[2], _clkTxt[3]) + + -- Coordinates (inside circle, near bottom) + coordFs = overlayFrame:CreateFontString(nil, "OVERLAY") + coordFs:SetFont(SFrames:GetFont(), 9, "OUTLINE") + coordFs:SetPoint("BOTTOM", Minimap, "BOTTOM", 0, 8) + coordFs:SetJustifyH("CENTER") + local _coordTxt = _A.coordText or { 0.80, 0.80, 0.80 } + coordFs:SetTextColor(_coordTxt[1], _coordTxt[2], _coordTxt[3]) + + MM.container = container + MM.overlayFrame = overlayFrame +end + +-------------------------------------------------------------------------------- +-- Interactions +-------------------------------------------------------------------------------- +local function SetupScrollZoom() + Minimap:EnableMouseWheel(true) + Minimap:SetScript("OnMouseWheel", function() + if arg1 > 0 then + Minimap_ZoomIn() + else + Minimap_ZoomOut() + end + end) +end + +local function SetupMouseHandler() + Minimap:SetScript("OnMouseUp", function() + if arg1 == "RightButton" then + if MiniMapTrackingDropDown then + ToggleDropDownMenu(1, nil, MiniMapTrackingDropDown, "cursor") + end + else + if Minimap_OnClick then + Minimap_OnClick(this) + end + end + end) +end + +MM.ApplyPosition = ApplyPosition + +-------------------------------------------------------------------------------- +-- Reposition child icons +-------------------------------------------------------------------------------- +local function SetupMailDragging() + if not MiniMapMailFrame then return end + + MiniMapMailFrame:SetMovable(true) + MiniMapMailFrame:EnableMouse(true) + MiniMapMailFrame:RegisterForDrag("LeftButton") + + MiniMapMailFrame:SetScript("OnDragStart", function() + MiniMapMailFrame:StartMoving() + end) + + MiniMapMailFrame:SetScript("OnDragStop", function() + MiniMapMailFrame:StopMovingOrSizing() + local db = GetDB() + local cx, cy = MiniMapMailFrame:GetCenter() + local ux, uy = UIParent:GetCenter() + if cx and cy and ux and uy then + db.mailIconX = cx - ux + db.mailIconY = cy - uy + end + end) +end + +local function RepositionIcons() + if MiniMapMailFrame then + local db = GetDB() + MiniMapMailFrame:ClearAllPoints() + if db.mailIconX and db.mailIconY then + MiniMapMailFrame:SetPoint("CENTER", UIParent, "CENTER", db.mailIconX, db.mailIconY) + else + local mapDiam = Minimap:GetWidth() + MiniMapMailFrame:SetPoint("RIGHT", Minimap, "RIGHT", 8, mapDiam * 0.12) + end + MiniMapMailFrame:SetFrameStrata("LOW") + MiniMapMailFrame:SetFrameLevel((overlayFrame and overlayFrame:GetFrameLevel() or 5) + 2) + SetupMailDragging() + end + + if MiniMapBattlefieldFrame then + MiniMapBattlefieldFrame:ClearAllPoints() + MiniMapBattlefieldFrame:SetPoint("BOTTOMLEFT", Minimap, "BOTTOM", 15, -8) + MiniMapBattlefieldFrame:SetFrameStrata("LOW") + MiniMapBattlefieldFrame:SetFrameLevel((overlayFrame and overlayFrame:GetFrameLevel() or 5) + 2) + end +end + +-------------------------------------------------------------------------------- +-- Update helpers +-------------------------------------------------------------------------------- +local function UpdateZoneText() + if not zoneFs then return end + local text = GetMinimapZoneText and GetMinimapZoneText() or "" + lastZoneText = text + zoneFs:SetText(text) +end + +local function SetZoneMap() + if SetMapToCurrentZone then + pcall(SetMapToCurrentZone) + end +end + +local clockTimer = 0 +local coordTimer = 0 +local zoneTimer = 0 +local lastZoneText = "" + +local function OnUpdate() + local dt = arg1 or 0 + local db = GetDB() + + -- Clock (every 1 s) + clockTimer = clockTimer + dt + if clockTimer >= 1 then + clockTimer = 0 + if db.showClock and clockFs then + local timeStr + if date then + timeStr = date("%H:%M") + else + local h, m = GetGameTime() + timeStr = string.format("%02d:%02d", h, m) + end + clockFs:SetText(timeStr) + clockFs:Show() + if clockBg then clockBg:Show() end + elseif clockFs then + clockFs:Hide() + if clockBg then clockBg:Hide() end + end + end + + -- Zone text (every 1 s) – catches late API updates missed by events + zoneTimer = zoneTimer + dt + if zoneTimer >= 1 then + zoneTimer = 0 + if zoneFs and GetMinimapZoneText then + local cur = GetMinimapZoneText() or "" + if cur ~= lastZoneText then + lastZoneText = cur + zoneFs:SetText(cur) + end + end + end + + -- Coordinates (every 0.25 s) + coordTimer = coordTimer + dt + if coordTimer >= 0.25 then + coordTimer = 0 + if db.showCoords and coordFs and GetPlayerMapPosition then + local ok, x, y = pcall(GetPlayerMapPosition, "player") + if ok and x and y and (x > 0 or y > 0) then + coordFs:SetText(string.format("%.1f, %.1f", x * 100, y * 100)) + coordFs:Show() + else + coordFs:Hide() + end + elseif coordFs then + coordFs:Hide() + end + end +end + +-------------------------------------------------------------------------------- +-- Public API +-------------------------------------------------------------------------------- +function MM:Refresh() + if not container then return end + + local fs = FrameSize() + local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs)) + + container:SetWidth(fs) + container:SetHeight(fs) + + Minimap:ClearAllPoints() + Minimap:SetPoint("CENTER", container, "TOPLEFT", + S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs)) + Minimap:SetWidth(mapDiam) + Minimap:SetHeight(mapDiam) + + if zoneFs then + local style = GetCurrentStyle() + local pcy = S(PLATE_Y + PLATE_H / 2 + (style.plateY or -6), fs) + zoneFs:ClearAllPoints() + zoneFs:SetPoint("CENTER", container, "TOP", 0, -pcy) + zoneFs:SetWidth(S(PLATE_W + 60, fs)) + local tc = style.textColor or {0.22, 0.13, 0.07} + zoneFs:SetTextColor(tc[1], tc[2], tc[3]) + end + + if clockBg then + clockBg:ClearAllPoints() + clockBg:SetPoint("CENTER", container, "BOTTOM", 0, + S((TEX_SIZE - CIRCLE_CY - CIRCLE_R) / 2, fs)) + end + + if coordFs then + coordFs:ClearAllPoints() + coordFs:SetPoint("BOTTOM", Minimap, "BOTTOM", 0, 8) + end + + if overlayTex then + overlayTex:SetTexture(GetMapTexture()) + end + + UpdateZoneText() + RepositionIcons() +end + +MM.MAP_STYLES = MAP_STYLES + +-------------------------------------------------------------------------------- +-- Shield: re-apply our skin after other addons (ShaguTweaks etc.) touch Minimap +-------------------------------------------------------------------------------- +local shielded = false + +local function ShieldMinimap() + if shielded then return end + shielded = true + + -- Override any external changes to Minimap parent / position / size + if Minimap:GetParent() ~= container then + Minimap:SetParent(container) + end + + local fs = FrameSize() + local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs)) + Minimap:ClearAllPoints() + Minimap:SetPoint("CENTER", container, "TOPLEFT", + S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs)) + Minimap:SetWidth(mapDiam) + Minimap:SetHeight(mapDiam) + Minimap:SetFrameStrata("LOW") + Minimap:SetFrameLevel(2) + Minimap:Show() + + if Minimap.SetMaskTexture then + Minimap:SetMaskTexture("Textures\\MinimapMask") + end + + HideDefaultElements() + + -- Kill any border/backdrop that ShaguTweaks may have injected + local regions = { Minimap:GetRegions() } + for _, r in ipairs(regions) do + if r and r:IsObjectType("Texture") then + local tex = r.GetTexture and r:GetTexture() + if tex and type(tex) == "string" then + local low = string.lower(tex) + if string.find(low, "border") or string.find(low, "backdrop") + or string.find(low, "overlay") or string.find(low, "background") then + r:Hide() + end + end + end + end + + shielded = false +end + +function MM:Initialize() + if not Minimap then return end + + local db = GetDB() + if db.enabled == false then return end + + local ok, err = pcall(function() + -- Build first (reparents Minimap), THEN hide old chrome + BuildFrame() + HideDefaultElements() + + -- Ensure Minimap is visible after reparent + Minimap:Show() + + SetupScrollZoom() + SetupMouseHandler() + + -- Apply position from settings + ApplyPosition() + + RepositionIcons() + + -- Zone text events + SFrames:RegisterEvent("ZONE_CHANGED", function() + SetZoneMap() + UpdateZoneText() + end) + SFrames:RegisterEvent("ZONE_CHANGED_NEW_AREA", function() + SetZoneMap() + UpdateZoneText() + end) + SFrames:RegisterEvent("ZONE_CHANGED_INDOORS", function() + SetZoneMap() + UpdateZoneText() + end) + + -- Re-apply after other addons finish loading (ShaguTweaks etc.) + SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() + pcall(ShieldMinimap) + pcall(UpdateZoneText) + end) + + -- Delayed zone text: GetMinimapZoneText() may be empty at PLAYER_LOGIN + local zoneDelay = CreateFrame("Frame") + local elapsed = 0 + zoneDelay:SetScript("OnUpdate", function() + elapsed = elapsed + (arg1 or 0) + if elapsed >= 1 then + zoneDelay:SetScript("OnUpdate", nil) + pcall(SetZoneMap) + pcall(UpdateZoneText) + end + end) + + -- Tick + container:SetScript("OnUpdate", OnUpdate) + + -- First refresh + SetZoneMap() + UpdateZoneText() + MM:Refresh() + end) + + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI Minimap error: " .. tostring(err) .. "|r") + end +end diff --git a/MinimapBuffs.lua b/MinimapBuffs.lua new file mode 100644 index 0000000..aa5cd81 --- /dev/null +++ b/MinimapBuffs.lua @@ -0,0 +1,638 @@ +SFrames.MinimapBuffs = {} + +local MB = SFrames.MinimapBuffs +local MAX_BUFFS = 32 +local MAX_DEBUFFS = 16 +local UPDATE_INTERVAL = 0.2 + +local BASE_X = -209 +local BASE_Y = -26 + +local ROUND_BACKDROP = { + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 8, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, +} + +local function GetDB() + if not SFramesDB or type(SFramesDB.MinimapBuffs) ~= "table" then + return { + enabled = true, iconSize = 30, iconsPerRow = 8, + spacing = 2, growDirection = "LEFT", position = "TOPRIGHT", + offsetX = 0, offsetY = 0, showTimer = true, + showDebuffs = true, debuffIconSize = 30, + } + end + return SFramesDB.MinimapBuffs +end + +local function FormatBuffTime(seconds) + if not seconds or seconds <= 0 or seconds >= 99999 then + return "N/A" + end + if seconds >= 3600 then + local h = math.floor(seconds / 3600) + local m = math.floor(math.mod(seconds, 3600) / 60) + return h .. "h" .. m .. "m" + elseif seconds >= 60 then + return math.floor(seconds / 60) .. "m" + else + return math.floor(seconds) .. "s" + end +end + +local DEBUFF_TYPE_COLORS = { + Magic = { r = 0.20, g = 0.60, b = 1.00 }, + Curse = { r = 0.60, g = 0.00, b = 1.00 }, + Disease = { r = 0.60, g = 0.40, b = 0.00 }, + Poison = { r = 0.00, g = 0.60, b = 0.00 }, +} +local DEBUFF_DEFAULT_COLOR = { r = 0.80, g = 0.00, b = 0.00 } +local WEAPON_ENCHANT_COLOR = { r = 0.58, g = 0.22, b = 0.82 } + +local function HideBlizzardBuffs() + for i = 0, 23 do + local btn = _G["BuffButton" .. i] + if btn then + btn:Hide() + btn:UnregisterAllEvents() + btn.Show = function() end + end + end + for i = 1, 3 do + local te = _G["TempEnchant" .. i] + if te then + te:Hide() + te:UnregisterAllEvents() + te.Show = function() end + end + end + if BuffFrame then + BuffFrame:Hide() + BuffFrame:UnregisterAllEvents() + BuffFrame.Show = function() end + end + if TemporaryEnchantFrame then + TemporaryEnchantFrame:Hide() + TemporaryEnchantFrame:UnregisterAllEvents() + TemporaryEnchantFrame.Show = function() end + end +end + +local function ApplySlotBackdrop(btn, isBuff) + btn:SetBackdrop(ROUND_BACKDROP) + btn:SetBackdropColor(0.06, 0.06, 0.08, 0.92) + if isBuff then + btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1) + else + local c = DEBUFF_DEFAULT_COLOR + btn:SetBackdropBorderColor(c.r, c.g, c.b, 1) + end +end + +local function CreateSlot(parent, namePrefix, index, isBuff) + local db = GetDB() + local size = isBuff and (db.iconSize or 30) or (db.debuffIconSize or 30) + + local btn = CreateFrame("Button", namePrefix .. index, parent) + btn:SetWidth(size) + btn:SetHeight(size) + ApplySlotBackdrop(btn, isBuff) + + btn.icon = btn:CreateTexture(nil, "ARTWORK") + btn.icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2) + btn.icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2) + btn.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + + btn.count = SFrames:CreateFontString(btn, 10, "RIGHT") + btn.count:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -1, -1) + btn.count:SetTextColor(1, 1, 1) + btn.count:SetShadowColor(0, 0, 0, 1) + btn.count:SetShadowOffset(1, -1) + + btn.timer = SFrames:CreateFontString(btn, 9, "CENTER") + btn.timer:SetPoint("BOTTOM", btn, "BOTTOM", 0, -11) + btn.timer:SetTextColor(1, 0.82, 0) + btn.timer:SetShadowColor(0, 0, 0, 1) + btn.timer:SetShadowOffset(1, -1) + + btn.isBuff = isBuff + btn:EnableMouse(true) + btn:RegisterForClicks("RightButtonUp") + + btn:SetScript("OnEnter", function() + if this._sfSimulated then + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT") + GameTooltip:AddLine(this._sfSimLabel or "Simulated", 1, 1, 1) + GameTooltip:AddLine(this._sfSimDesc or "", 0.7, 0.7, 0.7) + GameTooltip:Show() + return + end + if this._isWeaponEnchant and this._weaponSlotID then + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT") + GameTooltip:SetInventoryItem("player", this._weaponSlotID) + return + end + if this.buffIndex and this.buffIndex >= 0 then + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT") + GameTooltip:SetPlayerBuff(this.buffIndex) + end + end) + btn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + btn:SetScript("OnClick", function() + if this._sfSimulated then return end + if this._isWeaponEnchant then return end + if this.isBuff and this.buffIndex and this.buffIndex >= 0 then + CancelPlayerBuff(this.buffIndex) + end + end) + + btn.buffIndex = -1 + btn._sfSimulated = false + btn._isWeaponEnchant = false + btn._weaponSlotID = nil + btn:Hide() + return btn +end + +function MB:ApplyLayout() + if not self.buffSlots then return end + local db = GetDB() + local size = db.iconSize or 30 + local spacing = db.spacing or 2 + local perRow = db.iconsPerRow or 8 + local growLeft = (db.growDirection == "LEFT") + + for i = 1, MAX_BUFFS do + local btn = self.buffSlots[i] + btn:SetWidth(size) + btn:SetHeight(size) + btn:ClearAllPoints() + + local col = math.mod(i - 1, perRow) + local row = math.floor((i - 1) / perRow) + + local xDir = growLeft and -1 or 1 + local xOfs = col * (size + spacing) * xDir + local yOfs = -row * (size + 14 + spacing) + + local anchor = growLeft and "TOPRIGHT" or "TOPLEFT" + btn:SetPoint(anchor, self.buffContainer, anchor, xOfs, yOfs) + end + + if not self.debuffSlots then return end + local dSize = db.debuffIconSize or 30 + for i = 1, MAX_DEBUFFS do + local btn = self.debuffSlots[i] + btn:SetWidth(dSize) + btn:SetHeight(dSize) + btn:ClearAllPoints() + + local col = math.mod(i - 1, perRow) + local row = math.floor((i - 1) / perRow) + + local xDir = growLeft and -1 or 1 + local xOfs = col * (dSize + spacing) * xDir + local yOfs = -row * (dSize + 14 + spacing) + + local anchor = growLeft and "TOPRIGHT" or "TOPLEFT" + btn:SetPoint(anchor, self.debuffContainer, anchor, xOfs, yOfs) + end +end + +local function CountVisibleRows(slots, maxSlots, perRow) + local maxVisible = 0 + for i = 1, maxSlots do + if slots[i]:IsShown() then maxVisible = i end + end + if maxVisible == 0 then return 0 end + return math.floor((maxVisible - 1) / perRow) + 1 +end + +function MB:ApplyPosition() + if not self.buffContainer then return end + local db = GetDB() + local pos = db.position or "TOPRIGHT" + local x = BASE_X + (db.offsetX or 0) + local y = BASE_Y + (db.offsetY or 0) + + self.buffContainer:ClearAllPoints() + self.buffContainer:SetPoint(pos, UIParent, pos, x, y) + + self:AnchorDebuffs() +end + +function MB:AnchorDebuffs() + if not self.debuffContainer or not self.buffContainer then return end + local db = GetDB() + local size = db.iconSize or 30 + local spacing = db.spacing or 2 + local perRow = db.iconsPerRow or 8 + local rows = CountVisibleRows(self.buffSlots, MAX_BUFFS, perRow) + local rowHeight = size + 14 + spacing + local gap = 6 + + self.debuffContainer:ClearAllPoints() + self.debuffContainer:SetPoint("TOPRIGHT", self.buffContainer, "TOPRIGHT", 0, -(rows * rowHeight + gap)) +end + +local function ApplyTimerColor(btn, timeText) + if timeText == "N/A" then + btn.timer:SetTextColor(0.6, 0.6, 0.6) + return + end + local secs = nil + local _, _, hVal = string.find(timeText, "(%d+)h") + local _, _, mVal = string.find(timeText, "(%d+)m") + local _, _, sVal = string.find(timeText, "(%d+)s") + local h = tonumber(hVal) + local m = tonumber(mVal) + local s = tonumber(sVal) + if h then + secs = h * 3600 + (m or 0) * 60 + elseif m then + secs = m * 60 + elseif s then + secs = s + end + if secs and secs < 30 then + btn.timer:SetTextColor(1, 0.3, 0.3) + elseif secs and secs < 120 then + btn.timer:SetTextColor(1, 0.82, 0) + else + btn.timer:SetTextColor(0.8, 1, 0.8) + end +end + +local function SetTimerFromSeconds(btn, timeLeft, untilCancelled, showTimer) + if not showTimer then + btn.timer:Hide() + return + end + if untilCancelled == 1 or not timeLeft or timeLeft == 0 or timeLeft >= 99999 then + btn.timer:SetText("N/A") + btn.timer:SetTextColor(0.6, 0.6, 0.6) + else + btn.timer:SetText(FormatBuffTime(timeLeft)) + if timeLeft < 30 then + btn.timer:SetTextColor(1, 0.3, 0.3) + elseif timeLeft < 120 then + btn.timer:SetTextColor(1, 0.82, 0) + else + btn.timer:SetTextColor(0.8, 1, 0.8) + end + end + btn.timer:Show() +end + +function MB:UpdateBuffs() + if not self.buffSlots then return end + if self._simulating then return end + local db = GetDB() + local showTimer = db.showTimer ~= false + + local slotIdx = 0 + for i = 0, 31 do + local buffIndex, untilCancelled = GetPlayerBuff(i, "HELPFUL") + if buffIndex and buffIndex >= 0 then + slotIdx = slotIdx + 1 + if slotIdx > MAX_BUFFS then break end + + local btn = self.buffSlots[slotIdx] + local texture = GetPlayerBuffTexture(buffIndex) + if texture then + btn.icon:SetTexture(texture) + btn.buffIndex = buffIndex + btn._sfSimulated = false + btn._isWeaponEnchant = false + btn._weaponSlotID = nil + + local apps = GetPlayerBuffApplications(buffIndex) + if apps and apps > 1 then + btn.count:SetText(apps) + btn.count:Show() + else + btn.count:SetText("") + btn.count:Hide() + end + + local timeLeft = GetPlayerBuffTimeLeft(buffIndex) + SetTimerFromSeconds(btn, timeLeft, untilCancelled, showTimer) + + btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1) + btn:Show() + else + btn:Hide() + btn.buffIndex = -1 + btn._isWeaponEnchant = false + btn._weaponSlotID = nil + end + end + end + + local hasMainHandEnchant, mainHandExpiration, mainHandCharges, + hasOffHandEnchant, offHandExpiration, offHandCharges = GetWeaponEnchantInfo() + + if hasMainHandEnchant then + slotIdx = slotIdx + 1 + if slotIdx <= MAX_BUFFS then + local btn = self.buffSlots[slotIdx] + local texture = GetInventoryItemTexture("player", 16) + if texture then + btn.icon:SetTexture(texture) + btn.buffIndex = -1 + btn._sfSimulated = false + btn._isWeaponEnchant = true + btn._weaponSlotID = 16 + + if mainHandCharges and mainHandCharges > 0 then + btn.count:SetText(mainHandCharges) + btn.count:Show() + else + btn.count:SetText("") + btn.count:Hide() + end + + local timeLeft = mainHandExpiration and (mainHandExpiration / 1000) or 0 + SetTimerFromSeconds(btn, timeLeft, 0, showTimer) + + btn:SetBackdropBorderColor(WEAPON_ENCHANT_COLOR.r, WEAPON_ENCHANT_COLOR.g, WEAPON_ENCHANT_COLOR.b, 1) + btn:Show() + end + end + end + + if hasOffHandEnchant then + slotIdx = slotIdx + 1 + if slotIdx <= MAX_BUFFS then + local btn = self.buffSlots[slotIdx] + local texture = GetInventoryItemTexture("player", 17) + if texture then + btn.icon:SetTexture(texture) + btn.buffIndex = -1 + btn._sfSimulated = false + btn._isWeaponEnchant = true + btn._weaponSlotID = 17 + + if offHandCharges and offHandCharges > 0 then + btn.count:SetText(offHandCharges) + btn.count:Show() + else + btn.count:SetText("") + btn.count:Hide() + end + + local timeLeft = offHandExpiration and (offHandExpiration / 1000) or 0 + SetTimerFromSeconds(btn, timeLeft, 0, showTimer) + + btn:SetBackdropBorderColor(WEAPON_ENCHANT_COLOR.r, WEAPON_ENCHANT_COLOR.g, WEAPON_ENCHANT_COLOR.b, 1) + btn:Show() + end + end + end + + for j = slotIdx + 1, MAX_BUFFS do + local btn = self.buffSlots[j] + btn:Hide() + btn.buffIndex = -1 + btn._isWeaponEnchant = false + btn._weaponSlotID = nil + end + + self:UpdateDebuffs() + self:AnchorDebuffs() +end + +function MB:UpdateDebuffs() + if not self.debuffSlots then return end + if self._simulating then return end + local db = GetDB() + local showTimer = db.showTimer ~= false + + if db.showDebuffs == false then + for i = 1, MAX_DEBUFFS do + self.debuffSlots[i]:Hide() + end + if self.debuffContainer then self.debuffContainer:Hide() end + return + end + if self.debuffContainer then self.debuffContainer:Show() end + + local slotIdx = 0 + for i = 0, 15 do + local buffIndex, untilCancelled = GetPlayerBuff(i, "HARMFUL") + if buffIndex and buffIndex >= 0 then + slotIdx = slotIdx + 1 + if slotIdx > MAX_DEBUFFS then break end + + local btn = self.debuffSlots[slotIdx] + local texture = GetPlayerBuffTexture(buffIndex) + if texture then + btn.icon:SetTexture(texture) + btn.buffIndex = buffIndex + btn._sfSimulated = false + + local apps = GetPlayerBuffApplications(buffIndex) + if apps and apps > 1 then + btn.count:SetText(apps) + btn.count:Show() + else + btn.count:SetText("") + btn.count:Hide() + end + + local timeLeft = GetPlayerBuffTimeLeft(buffIndex) + SetTimerFromSeconds(btn, timeLeft, untilCancelled, showTimer) + + local debuffType = nil + SFrames.Tooltip:ClearLines() + SFrames.Tooltip:SetPlayerBuff(buffIndex) + local dTypeStr = SFramesScanTooltipTextRight1 and SFramesScanTooltipTextRight1:GetText() + if dTypeStr and dTypeStr ~= "" then debuffType = dTypeStr end + + local c = DEBUFF_TYPE_COLORS[debuffType] or DEBUFF_DEFAULT_COLOR + btn:SetBackdropBorderColor(c.r, c.g, c.b, 1) + + btn:Show() + else + btn:Hide() + btn.buffIndex = -1 + end + end + end + + for j = slotIdx + 1, MAX_DEBUFFS do + self.debuffSlots[j]:Hide() + self.debuffSlots[j].buffIndex = -1 + end +end + +-------------------------------------------------------------------------------- +-- Simulation (2 rows each for buff & debuff) +-------------------------------------------------------------------------------- +local SIM_BUFFS = { + -- Row 1 + { tex = "Interface\\Icons\\Spell_Holy_WordFortitude", label = "Power Word: Fortitude", desc = "Stamina +54", time = "N/A" }, + { tex = "Interface\\Icons\\Spell_Shadow_AntiShadow", label = "Shadow Protection", desc = "Shadow Resistance +60", time = "N/A" }, + { tex = "Interface\\Icons\\Spell_Holy_MagicalSentry", label = "Arcane Intellect", desc = "Intellect +31", time = "42m" }, + { tex = "Interface\\Icons\\Spell_Nature_Regeneration", label = "Mark of the Wild", desc = "All stats +12", time = "38m" }, + { tex = "Interface\\Icons\\Spell_Holy_GreaterBlessingofKings", label = "Blessing of Kings", desc = "All stats +10%", time = "7m" }, + { tex = "Interface\\Icons\\Spell_Holy_PrayerOfHealing02", label = "Renew", desc = "Heals 152 over 15 sec", time = "12s" }, + { tex = "Interface\\Icons\\Spell_Holy_DivineSpirit", label = "Divine Spirit", desc = "Spirit +40", time = "N/A" }, + { tex = "Interface\\Icons\\Spell_Fire_SealOfFire", label = "Fire Shield", desc = "Fire damage absorb", time = "25m" }, + -- Row 2 + { tex = "Interface\\Icons\\Spell_Holy_PowerWordShield", label = "Power Word: Shield", desc = "Absorbs 942 damage", time = "28s" }, + { tex = "Interface\\Icons\\Spell_Nature_Lightning", label = "Lightning Shield", desc = "3 charges", time = "9m", count = 3 }, + { tex = "Interface\\Icons\\Spell_Holy_SealOfWisdom", label = "Blessing of Wisdom", desc = "MP5 +33", time = "5m" }, + { tex = "Interface\\Icons\\Spell_Nature_UndyingStrength", label = "Thorns", desc = "Nature damage on hit", time = "N/A" }, + { tex = "Interface\\Icons\\Spell_Nature_Invisibilty", label = "Innervate", desc = "Spirit +400%", time = "18s" }, + { tex = "Interface\\Icons\\Spell_Holy_PowerInfusion", label = "Power Infusion", desc = "+20% spell damage", time = "14s" }, + { tex = "Interface\\Icons\\Spell_Holy_SealOfValor", label = "Blessing of Sanctuary", desc = "Block damage reduced", time = "3m" }, + { tex = "Interface\\Icons\\Spell_Nature_EnchantArmor", label = "Nature Resistance", desc = "Nature Resistance +60", time = "1h12m" }, +} + +local SIM_DEBUFFS = { + -- Row 1 + { tex = "Interface\\Icons\\Spell_Shadow_CurseOfTounable", label = "Curse of Tongues", desc = "Casting 50% slower", time = "28s", dtype = "Curse" }, + { tex = "Interface\\Icons\\Spell_Shadow_UnholyStrength", label = "Weakened Soul", desc = "Cannot be shielded", time = "15s", dtype = "Magic" }, + { tex = "Interface\\Icons\\Ability_Creature_Disease_02", label = "Corrupted Blood", desc = "Inflicts disease damage", time = "N/A", dtype = "Disease" }, + { tex = "Interface\\Icons\\Spell_Nature_CorrosiveBreath", label = "Deadly Poison", desc = "Inflicts Nature damage", time = "8s", dtype = "Poison" }, + { tex = "Interface\\Icons\\Spell_Shadow_Possession", label = "Fear", desc = "Feared for 8 sec", time = "6s", dtype = "Magic" }, + { tex = "Interface\\Icons\\Spell_Shadow_ShadowWordPain", label = "Shadow Word: Pain", desc = "Shadow damage over time", time = "24s", dtype = "Magic" }, + { tex = "Interface\\Icons\\Spell_Shadow_AbominationExplosion", label = "Mortal Strike", desc = "Healing reduced 50%", time = "5s", dtype = nil }, + { tex = "Interface\\Icons\\Spell_Frost_FrostArmor02", label = "Frostbolt", desc = "Movement slowed 40%", time = "4s", dtype = "Magic" }, + -- Row 2 + { tex = "Interface\\Icons\\Spell_Shadow_CurseOfSargeras", label = "Curse of Agony", desc = "Shadow damage over time", time = "22s", dtype = "Curse" }, + { tex = "Interface\\Icons\\Spell_Nature_Slow", label = "Crippling Poison", desc = "Movement slowed 70%", time = "10s", dtype = "Poison" }, + { tex = "Interface\\Icons\\Spell_Shadow_CurseOfMannoroth", label = "Curse of Elements", desc = "Resistance reduced 75", time = "N/A", dtype = "Curse" }, + { tex = "Interface\\Icons\\Ability_Creature_Disease_03", label = "Devouring Plague", desc = "Disease damage + heal", time = "20s", dtype = "Disease" }, +} + +function MB:SimulateBuffs() + if not self.buffSlots or not self.debuffSlots then return end + self._simulating = true + + for i = 1, MAX_BUFFS do + local btn = self.buffSlots[i] + local sim = SIM_BUFFS[i] + if sim then + btn.icon:SetTexture(sim.tex) + btn.buffIndex = -1 + btn._sfSimulated = true + btn._isWeaponEnchant = false + btn._weaponSlotID = nil + btn._sfSimLabel = sim.label + btn._sfSimDesc = sim.desc + + btn.timer:SetText(sim.time) + ApplyTimerColor(btn, sim.time) + btn.timer:Show() + + if sim.count and sim.count > 1 then + btn.count:SetText(sim.count) + btn.count:Show() + else + btn.count:Hide() + end + + btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1) + btn:Show() + else + btn:Hide() + end + end + + for i = 1, MAX_DEBUFFS do + local btn = self.debuffSlots[i] + local sim = SIM_DEBUFFS[i] + if sim then + btn.icon:SetTexture(sim.tex) + btn.buffIndex = -1 + btn._sfSimulated = true + btn._sfSimLabel = sim.label + btn._sfSimDesc = sim.desc + + btn.timer:SetText(sim.time) + ApplyTimerColor(btn, sim.time) + btn.timer:Show() + btn.count:Hide() + + local c = DEBUFF_TYPE_COLORS[sim.dtype] or DEBUFF_DEFAULT_COLOR + btn:SetBackdropBorderColor(c.r, c.g, c.b, 1) + btn:Show() + else + btn:Hide() + end + end + + if self.debuffContainer then self.debuffContainer:Show() end + self:AnchorDebuffs() +end + +function MB:StopSimulation() + self._simulating = false + self:UpdateBuffs() +end + +function MB:Refresh() + if not self.buffContainer then return end + local db = GetDB() + if db.enabled == false then + self.buffContainer:Hide() + if self.debuffContainer then self.debuffContainer:Hide() end + return + end + self:ApplyPosition() + self:ApplyLayout() + if not self._simulating then + self:UpdateBuffs() + else + self:AnchorDebuffs() + end + self.buffContainer:Show() +end + +function MB:Initialize() + local db = GetDB() + if db.enabled == false then return end + + HideBlizzardBuffs() + + self.buffContainer = CreateFrame("Frame", "SFramesMBuffContainer", UIParent) + self.buffContainer:SetWidth(400) + self.buffContainer:SetHeight(200) + self.buffContainer:SetFrameStrata("LOW") + + self.debuffContainer = CreateFrame("Frame", "SFramesMDebuffContainer", UIParent) + self.debuffContainer:SetWidth(400) + self.debuffContainer:SetHeight(100) + self.debuffContainer:SetFrameStrata("LOW") + + self.buffSlots = {} + for i = 1, MAX_BUFFS do + self.buffSlots[i] = CreateSlot(self.buffContainer, "SFramesMBuff", i, true) + end + + self.debuffSlots = {} + for i = 1, MAX_DEBUFFS do + self.debuffSlots[i] = CreateSlot(self.debuffContainer, "SFramesMDebuff", i, false) + end + + self:ApplyPosition() + self:ApplyLayout() + + self.updater = CreateFrame("Frame", nil, self.buffContainer) + self.updater.timer = 0 + self.updater:SetScript("OnUpdate", function() + this.timer = this.timer + arg1 + if this.timer >= UPDATE_INTERVAL then + MB:UpdateBuffs() + this.timer = 0 + end + end) + + self:UpdateBuffs() +end diff --git a/MinimapButton.lua b/MinimapButton.lua new file mode 100644 index 0000000..7c1c0d6 --- /dev/null +++ b/MinimapButton.lua @@ -0,0 +1,220 @@ +-------------------------------------------------------------------------------- +-- S-Frames: Minimap quick access button (MinimapButton.lua) +-- Left Click : open /nui UI settings +-- Right Click : open /nui bag settings +-- Shift + Drag: move icon around minimap +-------------------------------------------------------------------------------- + +SFrames.MinimapButton = SFrames.MinimapButton or {} + +local button = nil +local DEFAULT_ANGLE = 225 + +local function EnsureDB() + if not SFramesDB then + SFramesDB = {} + end + + if type(SFramesDB.minimapButton) ~= "table" then + SFramesDB.minimapButton = {} + end + + local db = SFramesDB.minimapButton + if type(db.angle) ~= "number" then + db.angle = DEFAULT_ANGLE + end + if db.hide == nil then + db.hide = false + end + + return db +end + +local function SafeAtan2(y, x) + if math.atan2 then + return math.atan2(y, x) + end + + if x > 0 then + return math.atan(y / x) + elseif x < 0 and y >= 0 then + return math.atan(y / x) + math.pi + elseif x < 0 and y < 0 then + return math.atan(y / x) - math.pi + elseif x == 0 and y > 0 then + return math.pi / 2 + elseif x == 0 and y < 0 then + return -math.pi / 2 + end + + return 0 +end + +local function GetOrbitRadius() + local w = Minimap and Minimap:GetWidth() or 140 + return w / 2 + 6 +end + +local function UpdatePosition() + if not button or not Minimap then + return + end + + local db = EnsureDB() + local radius = GetOrbitRadius() + local angleRad = math.rad(db.angle or DEFAULT_ANGLE) + local x = math.cos(angleRad) * radius + local y = math.sin(angleRad) * radius + + button:ClearAllPoints() + button:SetPoint("CENTER", Minimap, "CENTER", x, y) +end + +local function StartDrag() + if not button or not Minimap then + return + end + + button:SetScript("OnUpdate", function() + local mx, my = GetCursorPosition() + local scale = Minimap:GetEffectiveScale() or 1 + if scale == 0 then scale = 1 end + + mx = mx / scale + my = my / scale + + local cx, cy = Minimap:GetCenter() + if not cx or not cy then + return + end + + local angle = math.deg(SafeAtan2(my - cy, mx - cx)) + if angle < 0 then + angle = angle + 360 + end + + EnsureDB().angle = angle + UpdatePosition() + end) +end + +local function StopDrag() + if button then + button:SetScript("OnUpdate", nil) + end +end + +function SFrames.MinimapButton:Refresh() + local db = EnsureDB() + + if not button then + return + end + + if db.hide then + button:Hide() + else + button:Show() + end + + if button.icon and SFrames.SetIcon then + SFrames:SetIcon(button.icon, "logo") + local A = SFrames.ActiveTheme + if A and A.accentLight then + button.icon:SetVertexColor(A.accentLight[1], A.accentLight[2], A.accentLight[3], 1) + end + end + + UpdatePosition() +end + +function SFrames.MinimapButton:Initialize() + if button then + self:Refresh() + return + end + + if not Minimap then + return + end + + EnsureDB() + + button = CreateFrame("Button", "SFramesMinimapButton", Minimap) + button:SetWidth(32) + button:SetHeight(32) + button:SetFrameStrata("MEDIUM") + button:SetMovable(false) + button:RegisterForClicks("LeftButtonUp", "RightButtonUp") + button:RegisterForDrag("LeftButton") + + local border = button:CreateTexture(nil, "OVERLAY") + border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder") + border:SetWidth(56) + border:SetHeight(56) + border:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0) + + local bg = button:CreateTexture(nil, "BACKGROUND") + bg:SetTexture("Interface\\Minimap\\UI-Minimap-Background") + bg:SetWidth(20) + bg:SetHeight(20) + bg:SetPoint("CENTER", button, "CENTER", 0, 0) + + local icon = button:CreateTexture(nil, "ARTWORK") + icon:SetWidth(20) + icon:SetHeight(20) + icon:SetPoint("CENTER", button, "CENTER", 0, 0) + + local hl = button:CreateTexture(nil, "HIGHLIGHT") + hl:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight") + hl:SetBlendMode("ADD") + hl:SetWidth(31) + hl:SetHeight(31) + hl:SetPoint("CENTER", button, "CENTER", 0, 0) + + button.icon = icon + + button:SetScript("OnClick", function() + if arg1 == "RightButton" then + if SFrames.ConfigUI and SFrames.ConfigUI.Build then + SFrames.ConfigUI:Build("bags") + end + else + if SFrames.ConfigUI and SFrames.ConfigUI.Build then + SFrames.ConfigUI:Build("ui") + end + end + end) + + button:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_LEFT") + GameTooltip:ClearLines() + local A = SFrames.ActiveTheme + local tr, tg, tb = 1, 0.82, 0.94 + if A and A.accentLight then + tr, tg, tb = A.accentLight[1], A.accentLight[2], A.accentLight[3] + end + GameTooltip:AddLine("Nanami-UI", tr, tg, tb) + GameTooltip:AddLine("左键: 打开 UI 设置", 0.85, 0.85, 0.85) + GameTooltip:AddLine("右键: 打开背包设置", 0.85, 0.85, 0.85) + GameTooltip:AddLine("Shift+拖动: 移动图标", 0.6, 0.9, 0.6) + GameTooltip:Show() + end) + + button:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + button:SetScript("OnDragStart", function() + if not IsShiftKeyDown() then + return + end + StartDrag() + end) + + button:SetScript("OnDragStop", function() + StopDrag() + end) + + self:Refresh() +end diff --git a/Nanami-UI.toc b/Nanami-UI.toc new file mode 100644 index 0000000..1dd3af1 --- /dev/null +++ b/Nanami-UI.toc @@ -0,0 +1,67 @@ +## Interface: 11200 +## Title: Nanami-UI +## Notes: 现代极简猫系单位框架插件 - 喵~ (Nanami-UI) +## Author: AI Assistant +## Version: 1.0.0 +## OptionalDeps: ShaguTweaks, Blizzard_CombatLog, HealComm-1.0, DruidManaLib-1.0, !Libs, pfQuest +## SavedVariablesPerCharacter: SFramesDB +## SavedVariables: SFramesGlobalDB +Bindings.xml +Core.lua +Config.lua +Media.lua +IconMap.lua +Factory.lua +Chat.lua +Whisper.lua +ConfigUI.lua +SetupWizard.lua +GameMenu.lua +MinimapButton.lua +Minimap.lua +MapReveal.lua +WorldMap.lua +MapIcons.lua +Tweaks.lua +MinimapBuffs.lua +Focus.lua +ClassSkillData.lua +Units\Player.lua +Units\Pet.lua +Units\Target.lua +Units\ToT.lua +Units\Party.lua +Units\TalentTree.lua +SellPriceDB.lua +GearScore.lua +Tooltip.lua +Units\Raid.lua +ActionBars.lua + +Bags\Offline.lua +Bags\Sort.lua +Bags\Container.lua +Bags\Bank.lua +Bags\Features.lua +Bags\Core.lua +Merchant.lua +Trade.lua +Roll.lua +QuestUI.lua +BookUI.lua +QuestLogSkin.lua +TrainerUI.lua +TradeSkillDB.lua +TradeSkillUI.lua +CharacterPanel.lua +StatSummary.lua +InspectPanel.lua +SpellBookUI.lua +SocialUI.lua +FlightData.lua +FlightMap.lua +Mail.lua +PetStableSkin.lua +DarkmoonGuide.lua +DarkmoonMapMarker.lua +AFKScreen.lua diff --git a/PetStableSkin.lua b/PetStableSkin.lua new file mode 100644 index 0000000..ac6cd25 --- /dev/null +++ b/PetStableSkin.lua @@ -0,0 +1,565 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Pet Stable Skin (PetStableSkin.lua) +-- Skins the Blizzard PetStableFrame with Nanami-UI theme +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.PetStableSkin = {} +SFramesDB = SFramesDB or {} + +local T = SFrames.ActiveTheme +local skinned = false + +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function NukeTextures(frame, exceptions) + local regions = { frame:GetRegions() } + for _, r in ipairs(regions) do + if r:IsObjectType("Texture") and not r.sfKeep then + local rName = r:GetName() + local skip = false + if exceptions and rName then + for _, exc in ipairs(exceptions) do + if string.find(rName, exc) then + skip = true + break + end + end + end + if not skip then + r:SetTexture(nil) + r:SetAlpha(0) + r:Hide() + r.sfNuked = true + r:ClearAllPoints() + r:SetPoint("CENTER", UIParent, "CENTER", 9999, 9999) + end + end + end +end + +local function NukeChildTextures(frame) + local children = { frame:GetChildren() } + for _, child in ipairs(children) do + local cName = child:GetName() or "" + if string.find(cName, "Inset") or string.find(cName, "Bg") then + NukeTextures(child) + if child.SetBackdrop then child:SetBackdrop(nil) end + end + end +end + +local function SetRoundBackdrop(frame) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) +end + +local function CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.45) + s:SetBackdropBorderColor(0, 0, 0, 0.6) + s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) + return s +end + +local function MarkBackdropRegions(frame) + local regions = { frame:GetRegions() } + for _, r in ipairs(regions) do + if not r.sfNuked then r.sfKeep = true end + end +end + +-------------------------------------------------------------------------------- +-- Slot styling (ItemButton-type pet slot buttons) +-------------------------------------------------------------------------------- +local function StyleSlot(btn) + if not btn or btn.sfSkinned then return end + btn.sfSkinned = true + + local bname = btn:GetName() or "" + + local normalTex = _G[bname .. "NormalTexture"] + if normalTex then normalTex:SetAlpha(0) end + + NukeTextures(btn, { "Icon", "Count" }) + + local iconTex = _G[bname .. "IconTexture"] + if iconTex then + iconTex:SetTexCoord(0.08, 0.92, 0.08, 0.92) + iconTex.sfKeep = true + end + + local bg = btn:CreateTexture(nil, "BACKGROUND") + bg:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background") + bg:SetAllPoints() + bg:SetVertexColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.8) + bg.sfKeep = true + + local border = CreateFrame("Frame", nil, btn) + border:SetPoint("TOPLEFT", btn, "TOPLEFT", -2, 2) + border:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 2, -2) + border:SetBackdrop({ + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + border:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + btn.sfBorder = border + + local origEnter = btn:GetScript("OnEnter") + local origLeave = btn:GetScript("OnLeave") + + btn:SetScript("OnEnter", function() + if origEnter then origEnter() end + if this.sfBorder then + this.sfBorder:SetBackdropBorderColor( + T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4] or 1) + end + end) + + btn:SetScript("OnLeave", function() + if origLeave then origLeave() end + if this.sfBorder then + this.sfBorder:SetBackdropBorderColor( + T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + end + end) +end + +-------------------------------------------------------------------------------- +-- Close button styling +-------------------------------------------------------------------------------- +local function StyleCloseButton(btn) + if not btn or btn.sfSkinned then return end + btn.sfSkinned = true + + btn:SetWidth(22) + btn:SetHeight(22) + btn:ClearAllPoints() + btn:SetPoint("TOPRIGHT", btn:GetParent(), "TOPRIGHT", -6, -6) + + if btn.GetNormalTexture and btn:GetNormalTexture() then + btn:GetNormalTexture():SetAlpha(0) + end + if btn.GetPushedTexture and btn:GetPushedTexture() then + btn:GetPushedTexture():SetAlpha(0) + end + if btn.GetHighlightTexture and btn:GetHighlightTexture() then + btn:GetHighlightTexture():SetAlpha(0) + end + + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 8, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + + local cbg = { 0.5, 0.1, 0.1, 0.85 } + local cbd = { 0.6, 0.2, 0.2, 0.8 } + local cbgH = { 0.7, 0.15, 0.15, 1 } + local cbdH = { 0.9, 0.3, 0.3, 1 } + + btn:SetBackdropColor(cbg[1], cbg[2], cbg[3], cbg[4]) + btn:SetBackdropBorderColor(cbd[1], cbd[2], cbd[3], cbd[4]) + + local xLabel = btn:CreateFontString(nil, "OVERLAY") + xLabel:SetFont(GetFont(), 11, "OUTLINE") + xLabel:SetPoint("CENTER", 0, 0) + xLabel:SetText("X") + xLabel:SetTextColor(0.9, 0.8, 0.8) + + local origEnter = btn:GetScript("OnEnter") + local origLeave = btn:GetScript("OnLeave") + + btn:SetScript("OnEnter", function() + if origEnter then origEnter() end + this:SetBackdropColor(cbgH[1], cbgH[2], cbgH[3], cbgH[4]) + this:SetBackdropBorderColor(cbdH[1], cbdH[2], cbdH[3], cbdH[4]) + end) + + btn:SetScript("OnLeave", function() + if origLeave then origLeave() end + this:SetBackdropColor(cbg[1], cbg[2], cbg[3], cbg[4]) + this:SetBackdropBorderColor(cbd[1], cbd[2], cbd[3], cbd[4]) + end) +end + +-------------------------------------------------------------------------------- +-- UIPanelButton styling (purchase button etc.) +-------------------------------------------------------------------------------- +local function StyleActionButton(btn) + if not btn or btn.sfSkinned then return end + btn.sfSkinned = true + + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + + local bg = T.btnBg + local bd = T.btnBorder + btn:SetBackdropColor(bg[1], bg[2], bg[3], bg[4]) + btn:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4]) + + if btn.GetNormalTexture and btn:GetNormalTexture() then + btn:GetNormalTexture():SetTexture(nil) + end + if btn.GetPushedTexture and btn:GetPushedTexture() then + btn:GetPushedTexture():SetTexture(nil) + end + if btn.GetHighlightTexture and btn:GetHighlightTexture() then + btn:GetHighlightTexture():SetAlpha(0) + end + if btn.GetDisabledTexture and btn:GetDisabledTexture() then + btn:GetDisabledTexture():SetTexture(nil) + end + + local name = btn:GetName() or "" + for _, suffix in ipairs({ "Left", "Right", "Middle" }) do + local tex = _G[name .. suffix] + if tex then tex:SetAlpha(0); tex:Hide() end + end + + local fs = btn:GetFontString() + if fs then + fs:SetFont(GetFont(), 11, "OUTLINE") + fs:SetTextColor(T.text[1], T.text[2], T.text[3]) + end + + local hoverBg = T.btnHoverBg + local hoverBd = T.btnHoverBd + + local origEnter = btn:GetScript("OnEnter") + local origLeave = btn:GetScript("OnLeave") + + btn:SetScript("OnEnter", function() + if origEnter then origEnter() end + this:SetBackdropColor(hoverBg[1], hoverBg[2], hoverBg[3], hoverBg[4]) + this:SetBackdropBorderColor(hoverBd[1], hoverBd[2], hoverBd[3], hoverBd[4]) + end) + + btn:SetScript("OnLeave", function() + if origLeave then origLeave() end + this:SetBackdropColor(bg[1], bg[2], bg[3], bg[4]) + this:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4]) + end) +end + +-------------------------------------------------------------------------------- +-- Style all FontStrings on a frame with our themed font +-------------------------------------------------------------------------------- +local function StyleFontStrings(frame, size, color) + local regions = { frame:GetRegions() } + local font = GetFont() + for _, r in ipairs(regions) do + if r:IsObjectType("FontString") then + r:SetFont(font, size or 11, "OUTLINE") + if color then + r:SetTextColor(color[1], color[2], color[3]) + end + end + end +end + +-------------------------------------------------------------------------------- +-- Main skin application +-------------------------------------------------------------------------------- +local function ApplySkin() + if skinned then return end + if not PetStableFrame then return end + if SFramesDB.enablePetStable == false then return end + skinned = true + + local frame = PetStableFrame + local font = GetFont() + + -- 1) Remove Blizzard decorative textures from main frame + NukeTextures(frame, { "Icon", "Food", "Diet", "Selected" }) + + -- 2) Remove textures from sub-inset frames + NukeChildTextures(frame) + + -- 3) Apply themed backdrop + SetRoundBackdrop(frame) + MarkBackdropRegions(frame) + + -- 4) Shadow for depth (tagged so auto-compact ignores it) + local shadow = CreateShadow(frame) + shadow.sfOverlay = true + + -- 5) Free drag support with position persistence + frame:SetMovable(true) + frame:EnableMouse(true) + frame:SetClampedToScreen(true) + frame:RegisterForDrag("LeftButton") + frame:SetScript("OnDragStart", function() + this:StartMoving() + end) + frame:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + local point, _, relPoint, xOfs, yOfs = this:GetPoint() + SFramesDB.petStablePos = { + point = point, relPoint = relPoint, + x = xOfs, y = yOfs, + } + end) + + -- 6) Title + local titleFS = PetStableFrameTitleText + if titleFS then + titleFS:SetFont(font, 13, "OUTLINE") + titleFS:SetTextColor(T.title[1], T.title[2], T.title[3]) + titleFS.sfKeep = true + end + + -- 7) Close button + if PetStableFrameCloseButton then + StyleCloseButton(PetStableFrameCloseButton) + end + + -- 8) Style pet info FontStrings (try various naming patterns) + local infoFS = { + "PetStablePetName", "PetStableSelectedPetName", + "PetStablePetLevel", "PetStableSelectedPetLevel", + "PetStablePetFamily", "PetStableSelectedPetFamily", + "PetStablePetLoyalty", "PetStableSelectedPetLoyalty", + } + for _, n in ipairs(infoFS) do + local fs = _G[n] + if fs and fs.SetFont then + fs:SetFont(font, 11, "OUTLINE") + fs:SetTextColor(T.text[1], T.text[2], T.text[3]) + end + end + + local labelFS = { + "PetStableDietLabel", "PetStableTypeLabel", + "PetStableNameLabel", "PetStableLevelLabel", + "PetStableLoyaltyLabel", "PetStableFamilyLabel", + "PetStableDietText", + } + for _, n in ipairs(labelFS) do + local fs = _G[n] + if fs and fs.SetFont then + fs:SetFont(font, 10, "OUTLINE") + fs:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + end + end + + -- Also style any remaining FontStrings on the main frame + StyleFontStrings(frame, 11, T.text) + + -- 9) Current pet slot + if PetStableCurrentPet then + StyleSlot(PetStableCurrentPet) + end + + -- 10) Stable slots (dynamic count for Turtle WoW, check up to 20) + for i = 1, 20 do + local slot = _G["PetStableStableSlot" .. i] + if slot then + StyleSlot(slot) + end + end + -- Alternate naming pattern + for i = 1, 20 do + local slot = _G["PetStableSlot" .. i] + if slot and not slot.sfSkinned then + StyleSlot(slot) + end + end + + -- 11) Purchase button + if PetStablePurchaseButton then + StyleActionButton(PetStablePurchaseButton) + end + + -- 12) Model frame background (tagged so auto-compact ignores it) + if PetStableModel then + local modelBg = CreateFrame("Frame", nil, frame) + modelBg.sfOverlay = true + modelBg:SetPoint("TOPLEFT", PetStableModel, "TOPLEFT", -3, 3) + modelBg:SetPoint("BOTTOMRIGHT", PetStableModel, "BOTTOMRIGHT", 3, -3) + modelBg:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + modelBg:SetBackdropColor( + T.modelBg[1], T.modelBg[2], T.modelBg[3], T.modelBg[4] or 0.5) + modelBg:SetBackdropBorderColor( + T.modelBorder[1], T.modelBorder[2], T.modelBorder[3], T.modelBorder[4]) + modelBg:SetFrameLevel(math.max(0, PetStableModel:GetFrameLevel() - 1)) + end + + -- 13) Food/diet icons + for i = 1, 10 do + local food = _G["PetStableFood" .. i] or _G["PetStableDietIcon" .. i] + if food and food.SetTexCoord then + food:SetTexCoord(0.08, 0.92, 0.08, 0.92) + end + end + + -- 14) Any additional child frames that look like insets + local insets = { + "PetStableLeftInset", "PetStableBottomInset", + "PetStableRightInset", "PetStableTopInset", + } + for _, iname in ipairs(insets) do + local inset = _G[iname] + if inset then + NukeTextures(inset) + if inset.SetBackdrop then inset:SetBackdrop(nil) end + end + end + + -- 14b) Aggressive cleanup: clear backdrops/textures on ALL non-essential children + local knownFrames = {} + if PetStableModel then knownFrames[PetStableModel] = true end + if PetStableFrameCloseButton then knownFrames[PetStableFrameCloseButton] = true end + if PetStablePurchaseButton then knownFrames[PetStablePurchaseButton] = true end + if PetStableCurrentPet then knownFrames[PetStableCurrentPet] = true end + for si = 1, 20 do + local ss = _G["PetStableStableSlot" .. si] + if ss then knownFrames[ss] = true end + local ps = _G["PetStableSlot" .. si] + if ps then knownFrames[ps] = true end + end + local allCh = { frame:GetChildren() } + for _, child in ipairs(allCh) do + if not knownFrames[child] and not child.sfSkinned + and not child.sfOverlay and not child.sfBorder then + NukeTextures(child) + if child.SetBackdrop then child:SetBackdrop(nil) end + local subCh = { child:GetChildren() } + for _, sc in ipairs(subCh) do + NukeTextures(sc) + if sc.SetBackdrop then sc:SetBackdrop(nil) end + end + end + end + + -- 15) Auto-compact: measure left padding, then apply uniformly to right & bottom + local frameTop = frame:GetTop() + local frameLeft = frame:GetLeft() + local frameRight = frame:GetRight() + local frameBot = frame:GetBottom() + if frameTop and frameLeft and frameRight and frameBot then + local contentLeft = frameRight + local contentRight = frameLeft + local contentBot = frameTop + local function Scan(obj) + if not obj:IsShown() then return end + local l = obj.GetLeft and obj:GetLeft() + local r = obj.GetRight and obj:GetRight() + local b = obj.GetBottom and obj:GetBottom() + if l and l < contentLeft then contentLeft = l end + if r and r > contentRight then contentRight = r end + if b and b < contentBot then contentBot = b end + end + local children = { frame:GetChildren() } + for _, child in ipairs(children) do + if not child.sfOverlay and not child.sfBorder then + Scan(child) + end + end + local regions = { frame:GetRegions() } + for _, r in ipairs(regions) do + if not r.sfNuked and not r.sfKeep then + Scan(r) + end + end + -- Also scan named Blizzard content elements directly + local contentNames = { + "PetStableCurrentPet", "PetStablePurchaseButton", + "PetStableModel", "PetStableFrameCloseButton", + } + for i = 1, 20 do + table.insert(contentNames, "PetStableStableSlot" .. i) + table.insert(contentNames, "PetStableSlot" .. i) + end + for _, n in ipairs(contentNames) do + local obj = _G[n] + if obj and obj.IsShown and obj:IsShown() then Scan(obj) end + end + -- Scan visible FontStrings on the frame (they are content) + for _, r in ipairs(regions) do + if r:IsObjectType("FontString") and r:IsShown() and r:GetText() + and r:GetText() ~= "" then + local l = r:GetLeft() + local ri = r:GetRight() + local b = r:GetBottom() + if l and l < contentLeft then contentLeft = l end + if ri and ri > contentRight then contentRight = ri end + if b and b < contentBot then contentBot = b end + end + end + local leftPad = contentLeft - frameLeft + if leftPad < 4 then leftPad = 4 end + if leftPad > 16 then leftPad = 16 end + local newW = (contentRight - frameLeft) + leftPad + local newH = (frameTop - contentBot) + leftPad + if newW < frame:GetWidth() then + frame:SetWidth(newW) + end + if newH < frame:GetHeight() then + frame:SetHeight(newH) + end + end +end + +-------------------------------------------------------------------------------- +-- Restore saved position (runs every time the frame is shown) +-------------------------------------------------------------------------------- +local function RestorePosition() + if not PetStableFrame then return end + if not SFramesDB.petStablePos then return end + local pos = SFramesDB.petStablePos + PetStableFrame:ClearAllPoints() + PetStableFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) +end + +-------------------------------------------------------------------------------- +-- Event-driven skin application + position restore +-------------------------------------------------------------------------------- +local eventFrame = CreateFrame("Frame") +eventFrame:RegisterEvent("PET_STABLE_SHOW") +eventFrame:SetScript("OnEvent", function() + if event == "PET_STABLE_SHOW" then + ApplySkin() + RestorePosition() + end +end) + +if PetStableFrame then + local origOnShow = PetStableFrame:GetScript("OnShow") + PetStableFrame:SetScript("OnShow", function() + if origOnShow then origOnShow() end + ApplySkin() + RestorePosition() + end) +end diff --git a/QuestLogSkin.lua b/QuestLogSkin.lua new file mode 100644 index 0000000..2cbbb3c --- /dev/null +++ b/QuestLogSkin.lua @@ -0,0 +1,1308 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Quest Log Skin (QuestLogSkin.lua) +-- Reskins the default QuestLogFrame with Nanami-UI styled interface +-- Maintains full compatibility with pfquest and other quest addons +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.QuestLogSkin = {} +local QLS = SFrames.QuestLogSkin +SFramesDB = SFramesDB or {} + +-------------------------------------------------------------------------------- +-- Theme (Pink Cat-Paw) +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + objectiveComplete = { 0.45, 0.90, 0.45 }, + objectiveIncomplete = { 0.85, 0.78, 0.70 }, + tagElite = { 1.0, 0.65, 0.20 }, + tagDungeon = { 0.40, 0.70, 1.0 }, + tagRaid = { 0.70, 0.45, 1.0 }, + tagPvP = { 0.90, 0.30, 0.30 }, + tagComplete = { 0.40, 0.90, 0.40 }, +}) + +-------------------------------------------------------------------------------- +-- Layout +-------------------------------------------------------------------------------- +local FRAME_W = 595 +local LIST_W = 250 +local DETAIL_PAD = 8 +local HEADER_H = 30 +local BOTTOM_H = 36 +local INNER_PAD = 6 +local SCROLL_W = 14 +local LIST_BTN_W = LIST_W - INNER_PAD * 2 - SCROLL_W - 4 + +-------------------------------------------------------------------------------- +-- State +-------------------------------------------------------------------------------- +local skinApplied = false + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + local bg = bgColor or T.panelBg + local bd = borderColor or T.panelBorder + frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) + frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) +end + +local function CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0)) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.6) + s:SetBackdropBorderColor(0, 0, 0, 0.45) + return s +end + +local function StripTextures(frame) + if not frame then return end + local regions = { frame:GetRegions() } + for i = 1, table.getn(regions) do + local region = regions[i] + if region and region.SetTexture and not region._nanamiKeep then + local drawLayer = region.GetDrawLayer and region:GetDrawLayer() + if drawLayer == "BACKGROUND" or drawLayer == "BORDER" or drawLayer == "ARTWORK" then + region:SetTexture(nil) + region:SetAlpha(0) + region:Hide() + end + end + end +end + +local function StripAllTextures(frame) + if not frame then return end + local regions = { frame:GetRegions() } + for i = 1, table.getn(regions) do + local region = regions[i] + if region and region.SetTexture and not region._nanamiKeep then + region:SetTexture(nil) + region:SetAlpha(0) + region:Hide() + end + end +end + +local function StripNormalTexture(btn) + if not btn or not btn.GetNormalTexture then return end + local nt = btn:GetNormalTexture() + if nt then nt:SetTexture(nil); nt:SetAlpha(0) end +end + +local function StripPushedTexture(btn) + if not btn or not btn.GetPushedTexture then return end + local pt = btn:GetPushedTexture() + if pt then pt:SetTexture(nil); pt:SetAlpha(0) end +end + +local function StripHighlightTexture(btn) + if not btn or not btn.GetHighlightTexture then return end + local ht = btn:GetHighlightTexture() + if ht then ht:SetTexture(nil); ht:SetAlpha(0) end +end + +local function StripDisabledTexture(btn) + if not btn or not btn.GetDisabledTexture then return end + local dt = btn:GetDisabledTexture() + if dt then dt:SetTexture(nil); dt:SetAlpha(0) end +end + +local QUALITY_COLORS = { + [0] = { 0.62, 0.62, 0.62 }, [1] = { 1, 1, 1 }, + [2] = { 0.12, 1, 0 }, [3] = { 0.0, 0.44, 0.87 }, + [4] = { 0.64, 0.21, 0.93 }, [5] = { 1, 0.5, 0 }, +} + +local LINK_QUALITY_MAP = { + ["9d9d9d"] = 0, ["ffffff"] = 1, ["1eff00"] = 2, + ["0070dd"] = 3, ["a335ee"] = 4, ["ff8000"] = 5, + ["e6cc80"] = 6, +} + +local function QualityFromLink(link) + if not link then return nil end + local _, _, hex = string.find(link, "|c(%x%x%x%x%x%x%x%x)|") + if hex and string.len(hex) == 8 then + local color = string.lower(string.sub(hex, 3, 8)) + return LINK_QUALITY_MAP[color] + end + return nil +end + +local function GetQuestGreenRange() + local level = UnitLevel("player") or 60 + if level <= 5 then return 0 end + if level <= 39 then return math.floor(level / 10) + 5 end + return math.floor(level / 5) + 1 + 8 +end + +local function GetDiffColor(level) + local pLvl = UnitLevel("player") or 60 + local diff = (level or pLvl) - pLvl + if diff >= 5 then return 1, 0.10, 0.10 + elseif diff >= 3 then return 1, 0.50, 0.25 + elseif diff >= -2 then return 1, 1, 0 + elseif diff >= -GetQuestGreenRange() then return 0.25, 0.75, 0.25 + else return 0.50, 0.50, 0.50 + end +end + +-------------------------------------------------------------------------------- +-- Scrollbar Skinning +-------------------------------------------------------------------------------- +local function SkinScrollBar(scrollBarName) + local sb = _G[scrollBarName] + if not sb then return end + StripAllTextures(sb) + + local track = sb:CreateTexture(nil, "BACKGROUND") + track:SetTexture("Interface\\Buttons\\WHITE8X8") + track:SetVertexColor(T.scrollTrack[1], T.scrollTrack[2], T.scrollTrack[3], T.scrollTrack[4]) + track:SetWidth(6) + track:SetPoint("TOP", sb, "TOP", 0, -16) + track:SetPoint("BOTTOM", sb, "BOTTOM", 0, 16) + + local thumb = sb:GetThumbTexture() + if thumb then + thumb:SetTexture("Interface\\Buttons\\WHITE8X8") + thumb:SetVertexColor(T.scrollThumb[1], T.scrollThumb[2], T.scrollThumb[3], T.scrollThumb[4]) + thumb:SetWidth(6) + thumb:SetHeight(40) + end + + for _, suffix in ipairs({"ScrollUpButton", "ScrollDownButton"}) do + local b = _G[scrollBarName .. suffix] + if b then + StripAllTextures(b) + StripNormalTexture(b) + StripPushedTexture(b) + StripDisabledTexture(b) + StripHighlightTexture(b) + b:SetWidth(6) + b:SetHeight(6) + end + end +end + +-------------------------------------------------------------------------------- +-- Action Button Skinning +-------------------------------------------------------------------------------- +local btnDisabledBg = { 0.08, 0.04, 0.06, 0.60 } +local btnDisabledBd = { 0.22, 0.14, 0.18, 0.40 } +local btnDisabledText = { 0.35, 0.25, 0.30 } + +local function RefreshButtonDisabledLook(btn) + if not btn then return end + local disabled = false + if btn.IsEnabled then + local ok, result = pcall(btn.IsEnabled, btn) + if ok then disabled = not result end + end + if disabled then + SetRoundBackdrop(btn, btnDisabledBg, btnDisabledBd) + if btn.GetFontString then + local fs = btn:GetFontString() + if fs then fs:SetTextColor(btnDisabledText[1], btnDisabledText[2], btnDisabledText[3]) end + end + else + SetRoundBackdrop(btn, T.btnBg, T.btnBorder) + if btn.GetFontString then + local fs = btn:GetFontString() + if fs then fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end + end + end +end + +local function SkinActionButton(btn) + if not btn or btn._nanamiActionSkinned then return end + btn._nanamiActionSkinned = true + StripAllTextures(btn) + StripNormalTexture(btn) + StripPushedTexture(btn) + StripHighlightTexture(btn) + SetRoundBackdrop(btn, T.btnBg, T.btnBorder) + + if btn.GetFontString then + local fs = btn:GetFontString() + if fs then + fs:SetFont(GetFont(), 11, "OUTLINE") + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end + + local origEnter = btn:GetScript("OnEnter") + btn:SetScript("OnEnter", function() + local disabled = false + if this.IsEnabled then + local ok, r = pcall(this.IsEnabled, this) + if ok then disabled = not r end + end + if disabled then + if origEnter then origEnter() end + return + end + SetRoundBackdrop(this, T.btnHoverBg, T.btnHoverBd) + if this.GetFontString then + local tfs = this:GetFontString() + if tfs then tfs:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end + end + if origEnter then origEnter() end + end) + local origLeave = btn:GetScript("OnLeave") + btn:SetScript("OnLeave", function() + local disabled = false + if this.IsEnabled then + local ok, r = pcall(this.IsEnabled, this) + if ok then disabled = not r end + end + if disabled then + RefreshButtonDisabledLook(this) + if origLeave then origLeave() end + return + end + SetRoundBackdrop(this, T.btnBg, T.btnBorder) + if this.GetFontString then + local tfs = this:GetFontString() + if tfs then tfs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end + end + if origLeave then origLeave() end + end) + btn:SetScript("OnMouseDown", function() + local disabled = false + if this.IsEnabled then + local ok, r = pcall(this.IsEnabled, this) + if ok then disabled = not r end + end + if disabled then return end + SetRoundBackdrop(this, T.btnDownBg, T.btnBorder) + end) + btn:SetScript("OnMouseUp", function() + local disabled = false + if this.IsEnabled then + local ok, r = pcall(this.IsEnabled, this) + if ok then disabled = not r end + end + if disabled then return end + SetRoundBackdrop(this, T.btnHoverBg, T.btnHoverBd) + end) +end + +-------------------------------------------------------------------------------- +-- Reward Item Skinning +-------------------------------------------------------------------------------- +local function SkinRewardItem(btn) + if not btn or btn._nanamiSkinned then return end + btn._nanamiSkinned = true + + local bname = btn:GetName() or "" + + local iconTex = _G[bname .. "IconTexture"] + if iconTex then iconTex._nanamiKeep = true end + + StripAllTextures(btn) + StripNormalTexture(btn) + SetRoundBackdrop(btn, T.rewardBg, T.rewardBorder) + + if iconTex then + iconTex:SetTexCoord(0.08, 0.92, 0.08, 0.92) + iconTex:ClearAllPoints() + iconTex:SetPoint("LEFT", btn, "LEFT", 4, 0) + iconTex:SetWidth(30); iconTex:SetHeight(30) + iconTex:SetAlpha(1) + iconTex:Show() + end + local nameFrame = _G[bname .. "NameFrame"] + if nameFrame then nameFrame:SetTexture(nil); nameFrame:SetAlpha(0) end + local nameText = _G[bname .. "Name"] + if nameText then + nameText:SetFont(GetFont(), 11, "OUTLINE") + nameText:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + end + local countText = _G[bname .. "Count"] + if countText then countText:SetFont(GetFont(), 10, "OUTLINE") end +end + +-------------------------------------------------------------------------------- +-- Quest Log Item Quality Coloring +-------------------------------------------------------------------------------- +local function GetQuestLogItemQuality(itemType, index) + if itemType == "choice" and GetQuestLogChoiceInfo then + local ok, name, tex, num, q, usable = pcall(GetQuestLogChoiceInfo, index) + if ok and q and q > 0 then return q end + elseif itemType == "reward" and GetQuestLogRewardInfo then + local ok, name, tex, num, q, usable = pcall(GetQuestLogRewardInfo, index) + if ok and q and q > 0 then return q end + end + if GetQuestLogItemLink then + local ok, link = pcall(GetQuestLogItemLink, itemType, index) + if ok and link then + local q = QualityFromLink(link) + if q then return q end + if GetItemInfo then + local ok2, _, _, iq = pcall(GetItemInfo, link) + if ok2 and iq and iq > 0 then return iq end + end + end + end + return nil +end + +local function ApplyQuestLogItemQuality() + local choiceIdx = 0 + local rewardIdx = 0 + for i = 1, 10 do + local item = _G["QuestLogItem" .. i] + if not item or not item:IsVisible() then break end + + local bname = item:GetName() or "" + local nameText = _G[bname .. "Name"] + if not nameText then break end + + local itemType = item.type + local idx = 0 + if itemType == "choice" then + choiceIdx = choiceIdx + 1 + idx = choiceIdx + elseif itemType == "reward" then + rewardIdx = rewardIdx + 1 + idx = rewardIdx + else + break + end + + local quality = GetQuestLogItemQuality(itemType, idx) + local qc = quality and QUALITY_COLORS[quality] + if qc and quality >= 2 then + nameText:SetTextColor(qc[1], qc[2], qc[3]) + item:SetBackdropBorderColor(qc[1], qc[2], qc[3], 0.8) + elseif qc and quality == 1 then + nameText:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + item:SetBackdropBorderColor(T.rewardBorder[1], T.rewardBorder[2], T.rewardBorder[3], T.rewardBorder[4]) + else + nameText:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + item:SetBackdropBorderColor(T.rewardBorder[1], T.rewardBorder[2], T.rewardBorder[3], T.rewardBorder[4]) + end + end +end + +-------------------------------------------------------------------------------- +-- Tag Color for elite/dungeon/raid/pvp/complete +-------------------------------------------------------------------------------- +local function GetTagColor(tagText) + if not tagText or tagText == "" then return nil end + local t = string.lower(tagText) + if string.find(t, "elite") or string.find(t, "精英") then + return T.tagElite + elseif string.find(t, "dungeon") or string.find(t, "地下城") then + return T.tagDungeon + elseif string.find(t, "raid") or string.find(t, "团队") then + return T.tagRaid + elseif string.find(t, "pvp") then + return T.tagPvP + elseif string.find(t, "complete") or string.find(t, "完成") then + return T.tagComplete + end + return T.dimText +end + +-------------------------------------------------------------------------------- +-- Main Frame Skinning +-------------------------------------------------------------------------------- +local function SkinMainFrame() + local f = QuestLogFrame + if not f then return end + + -- Resize frame to remove excess right-side space + f:SetWidth(FRAME_W) + + StripTextures(f) + SetRoundBackdrop(f, T.panelBg, T.panelBorder) + CreateShadow(f) + + for _, pName in ipairs({"QuestLogFramePortrait", "QuestLogPortrait"}) do + local p = _G[pName] + if p then p:SetTexture(nil); p:SetAlpha(0); p:Hide() end + end + + local titleText = QuestLogTitleText + if titleText then + titleText:SetFont(GetFont(), 14, "OUTLINE") + titleText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + titleText:ClearAllPoints() + titleText:SetPoint("LEFT", f, "TOPLEFT", 14, -15) + end + + -- Quest count to top-right + local questCount = QuestLogQuestCount + if questCount then + questCount:SetFont(GetFont(), 11, "OUTLINE") + questCount:SetTextColor(T.countText[1], T.countText[2], T.countText[3]) + questCount:ClearAllPoints() + questCount:SetPoint("RIGHT", f, "TOPRIGHT", -30, -15) + end + + local closeBtn = _G["QuestLogFrameCloseButton"] + if closeBtn then + closeBtn:ClearAllPoints() + closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -2, -2) + closeBtn:SetWidth(22); closeBtn:SetHeight(22) + closeBtn:SetFrameLevel(f:GetFrameLevel() + 10) + end + + -- Make frame draggable and respond to ESC + if UIPanelWindows then + UIPanelWindows["QuestLogFrame"] = nil + end + table.insert(UISpecialFrames, "QuestLogFrame") + f:SetMovable(true) + f:EnableMouse(true) + f:SetClampedToScreen(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + -- Header separator + local sep = f:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetHeight(1) + sep:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -HEADER_H) + sep:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -HEADER_H) + sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + + -- Bottom separator + local bsep = f:CreateTexture(nil, "ARTWORK") + bsep:SetTexture("Interface\\Buttons\\WHITE8X8") + bsep:SetHeight(1) + bsep:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 8, BOTTOM_H) + bsep:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -8, BOTTOM_H) + bsep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + + -- Detect and preserve Turtle WoW translate / ? buttons + -- Also find any unrecognized buttons and ensure they remain visible + local knownPrefixes = { + "QuestLogTitle", "QuestLogFrame", "QuestLogCollapseAll", + "QuestLogTrack", "QuestFramePush", "pfQuest", + } + local function isKnownButton(child) + if child._nanamiPfSkinned or child._nanamiBottomBtn + or child._nanamiActionSkinned then return true end + local cname = child:GetName() or "" + if cname == "" then return false end + for ki = 1, table.getn(knownPrefixes) do + if string.find(cname, knownPrefixes[ki]) then return true end + end + return false + end + + f._skinExtraButtons = function() + local searchParents = {} + table.insert(searchParents, f) + local dc = _G["QuestLogDetailScrollFrameChildFrame"] + or _G["QuestLogDetailScrollFrameChild"] + if dc then table.insert(searchParents, dc) end + local ds = QuestLogDetailScrollFrame + if ds then table.insert(searchParents, ds) end + + local detailLevel = (f._nanamiDetailPanel and f._nanamiDetailPanel:GetFrameLevel()) or (f:GetFrameLevel() + 2) + + for pi = 1, table.getn(searchParents) do + local parent = searchParents[pi] + if parent and parent.GetChildren then + local children = { parent:GetChildren() } + for ci = 1, table.getn(children) do + local child = children[ci] + if child and child.GetObjectType then + local objType = child:GetObjectType() + if objType == "Button" and not isKnownButton(child) then + -- Ensure button is visible above detailPanel + if child:GetFrameLevel() <= detailLevel then + child:SetFrameLevel(detailLevel + 3) + end + child:Show() + SkinPfQuestButton(child) + end + end + end + end + end + + -- Also search for globally named Turtle WoW translate buttons + for _, gname in ipairs({ + "QuestTranslateButton", "QuestLogTranslateButton", + "QuestInfoTranslateButton", "TurtleTranslateButton", + "QuestTranslationButton", "QuestHelpButton", + }) do + local gb = _G[gname] + if gb and gb.GetObjectType and not gb._nanamiPfSkinned then + gb:Show() + if gb:GetFrameLevel() <= detailLevel then + gb:SetFrameLevel(detailLevel + 3) + end + SkinPfQuestButton(gb) + end + end + end +end + +-------------------------------------------------------------------------------- +-- Quest List Panel Skinning +-------------------------------------------------------------------------------- +local function SkinQuestList() + local listFrame = QuestLogListScrollFrame + if not listFrame then return end + local f = QuestLogFrame + + local listPanel = CreateFrame("Frame", nil, f) + listPanel:SetPoint("TOPLEFT", f, "TOPLEFT", DETAIL_PAD, -(HEADER_H + 2)) + listPanel:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", DETAIL_PAD, BOTTOM_H + 2) + listPanel:SetWidth(LIST_W) + SetRoundBackdrop(listPanel, T.listBg, T.listBorder) + listPanel:SetFrameLevel(f:GetFrameLevel() + 1) + f._nanamiListPanel = listPanel + + listFrame:ClearAllPoints() + listFrame:SetPoint("TOPLEFT", listPanel, "TOPLEFT", INNER_PAD, -INNER_PAD) + listFrame:SetPoint("BOTTOMRIGHT", listPanel, "BOTTOMRIGHT", -(SCROLL_W + INNER_PAD), 4) + listFrame:SetWidth(LIST_W - INNER_PAD * 2 - SCROLL_W) + + StripTextures(listFrame) + SkinScrollBar("QuestLogListScrollFrameScrollBar") + + local collapseAll = QuestLogCollapseAllButton + if collapseAll then + StripNormalTexture(collapseAll) + StripPushedTexture(collapseAll) + StripHighlightTexture(collapseAll) + end + + local highlight = _G["QuestLogHighlightFrame"] + if highlight then + StripAllTextures(highlight) + highlight:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + tile = true, tileSize = 16, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + highlight:SetBackdropColor(T.questSelected[1], T.questSelected[2], T.questSelected[3], T.questSelected[4]) + + local hlBar = highlight:CreateTexture(nil, "OVERLAY") + hlBar:SetTexture("Interface\\Buttons\\WHITE8X8") + hlBar:SetWidth(3) + hlBar:SetPoint("TOPLEFT", highlight, "TOPLEFT", 0, 0) + hlBar:SetPoint("BOTTOMLEFT", highlight, "BOTTOMLEFT", 0, 0) + hlBar:SetVertexColor(T.questSelBar[1], T.questSelBar[2], T.questSelBar[3], T.questSelBar[4]) + end +end + +-------------------------------------------------------------------------------- +-- Quest Detail Panel Skinning +-------------------------------------------------------------------------------- +local function SkinQuestDetail() + local detailFrame = QuestLogDetailScrollFrame + if not detailFrame then return end + local f = QuestLogFrame + + local DETAIL_W = FRAME_W - LIST_W - DETAIL_PAD * 2 - 4 + + local detailPanel = CreateFrame("Frame", nil, f) + detailPanel:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + DETAIL_PAD + 4, -(HEADER_H + 2)) + detailPanel:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -DETAIL_PAD, BOTTOM_H + 2) + SetRoundBackdrop(detailPanel, T.detailBg, T.detailBorder) + detailPanel:SetFrameLevel(f:GetFrameLevel() + 1) + f._nanamiDetailPanel = detailPanel + + detailFrame:ClearAllPoints() + detailFrame:SetPoint("TOPLEFT", detailPanel, "TOPLEFT", INNER_PAD, -INNER_PAD) + detailFrame:SetPoint("BOTTOMRIGHT", detailPanel, "BOTTOMRIGHT", -(SCROLL_W + INNER_PAD), INNER_PAD) + detailFrame:SetFrameLevel(detailPanel:GetFrameLevel() + 2) + + StripTextures(detailFrame) + SkinScrollBar("QuestLogDetailScrollFrameScrollBar") + + local contentW = DETAIL_W - INNER_PAD * 2 - SCROLL_W - 6 + local detailChild = _G["QuestLogDetailScrollFrameChildFrame"] + or _G["QuestLogDetailScrollFrameChild"] + if detailChild and contentW > 100 then + detailChild:SetWidth(contentW) + end + + if QuestLogNoQuestsText then + QuestLogNoQuestsText:SetFont(GetFont(), 13) + QuestLogNoQuestsText:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + end + if EmptyQuestLogFrame then + StripTextures(EmptyQuestLogFrame) + local et = _G["EmptyQuestLogFrameText"] + if et then + et:SetFont(GetFont(), 13) + et:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + end + end +end + +-------------------------------------------------------------------------------- +-- Bottom Buttons Skinning +-------------------------------------------------------------------------------- +local function SkinBottomButtons() + local f = QuestLogFrame + local btnW = math.floor((FRAME_W - 40) / 3) + if btnW > 120 then btnW = 120 end + if btnW < 80 then btnW = 80 end + + local abandonBtn = QuestLogFrameAbandonButton + if abandonBtn then + SkinActionButton(abandonBtn) + abandonBtn:SetHeight(24) + abandonBtn:ClearAllPoints() + abandonBtn:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 12, 6) + abandonBtn:SetWidth(btnW) + abandonBtn._nanamiBottomBtn = true + end + + local shareBtn = _G["QuestLogFramePushQuestButton"] or _G["QuestFramePushQuestButton"] + if shareBtn then + SkinActionButton(shareBtn) + shareBtn:SetHeight(24) + shareBtn:ClearAllPoints() + shareBtn:SetPoint("BOTTOM", f, "BOTTOM", 0, 6) + shareBtn:SetWidth(btnW) + shareBtn._nanamiBottomBtn = true + shareBtn._nanamiOrigText = nil + if shareBtn.GetFontString then + local fs = shareBtn:GetFontString() + if fs then shareBtn._nanamiOrigText = fs:GetText() end + end + + shareBtn._nanamiRefreshShare = function(sb) + if not sb then return end + local canShare = GetQuestLogPushable and GetQuestLogPushable() + local fs = sb.GetFontString and sb:GetFontString() + if not canShare then + SetRoundBackdrop(sb, btnDisabledBg, btnDisabledBd) + if fs then + fs:SetTextColor(btnDisabledText[1], btnDisabledText[2], btnDisabledText[3]) + end + else + SetRoundBackdrop(sb, T.btnBg, T.btnBorder) + if fs then + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end + end + + shareBtn._nanamiRefreshShare(shareBtn) + + local sharePoll = CreateFrame("Frame", nil, shareBtn) + sharePoll._t = 0 + sharePoll:SetScript("OnUpdate", function() + this._t = (this._t or 0) + (arg1 or 0) + if this._t < 1.0 then return end + this._t = 0 + local sb = this:GetParent() + if sb and sb._nanamiRefreshShare then sb._nanamiRefreshShare(sb) end + end) + end + + -- Search for exit button by multiple names and text content + local exitBtn = nil + for _, eName in ipairs({ + "QuestLogFrameExitButton", "QuestLogExitButton", + "QuestLogCancelButton", "QuestLogCloseButton", + "QuestLogFrameCancelButton", + }) do + if _G[eName] then exitBtn = _G[eName]; break end + end + + -- Fallback: find any bottom button with "退出"/"Exit"/"Close" text + if not exitBtn then + local children = { f:GetChildren() } + for ci = 1, table.getn(children) do + local child = children[ci] + if child and child.GetObjectType and child:GetObjectType() == "Button" + and child.GetFontString then + local cfs = child:GetFontString() + if cfs then + local txt = cfs:GetText() + if txt and (string.find(txt, "退出") or string.find(txt, "Exit") + or string.find(txt, "Close") or string.find(txt, "关闭")) then + exitBtn = child + break + end + end + end + end + end + + if exitBtn then + SkinActionButton(exitBtn) + exitBtn:SetHeight(24) + exitBtn:ClearAllPoints() + exitBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 6) + exitBtn:SetWidth(btnW) + exitBtn._nanamiBottomBtn = true + end + + -- Also skin any other unskinned bottom-area buttons (Turtle WoW additions) + local children = { f:GetChildren() } + for ci = 1, table.getn(children) do + local child = children[ci] + if child and child.GetObjectType and child:GetObjectType() == "Button" + and not child._nanamiBottomBtn and not child._nanamiPfSkinned + and not child._nanamiActionSkinned then + -- Check if this button is near the bottom of the frame + local point = child.GetPoint and child:GetPoint() + if point and (point == "BOTTOMLEFT" or point == "BOTTOMRIGHT" or point == "BOTTOM") then + if child.GetFontString then + local cfs = child:GetFontString() + if cfs then + local txt = cfs:GetText() + if txt and txt ~= "" then + SkinActionButton(child) + child._nanamiBottomBtn = true + end + end + end + end + end + end +end + +-------------------------------------------------------------------------------- +-- Quest List Entries Restyling +-------------------------------------------------------------------------------- +local function GetHighlightAnchorBtn() + local hl = _G["QuestLogHighlightFrame"] + if hl and hl.IsVisible and hl:IsVisible() and hl.GetPoint then + local ok, _, anchor = pcall(hl.GetPoint, hl) + if ok and anchor then return anchor end + end + return nil +end + +local function StyleQuestListEntries() + local numEntries, numQuests = 0, 0 + if GetNumQuestLogEntries then + numEntries, numQuests = GetNumQuestLogEntries() + end + local questsDisplayed = QUESTS_DISPLAYED or 22 + local hlBtn = GetHighlightAnchorBtn() + + for i = 1, questsDisplayed do + local btn = _G["QuestLogTitle" .. i] + if not btn then break end + + if not btn._nanamiSkinned then + StripPushedTexture(btn) + + local nt = _G["QuestLogTitle" .. i .. "NormalText"] + if nt then nt:SetFont(GetFont(), 11) end + local tg = _G["QuestLogTitle" .. i .. "Tag"] + if tg then tg:SetFont(GetFont(), 10, "OUTLINE") end + + local origHL = btn:GetHighlightTexture() + if origHL then + origHL:SetTexture("Interface\\Buttons\\WHITE8X8") + origHL:SetVertexColor(T.questHover[1], T.questHover[2], T.questHover[3], T.questHover[4]) + origHL:SetAllPoints(btn) + origHL:SetBlendMode("ADD") + end + + btn._nanamiCollapse = btn:CreateFontString(nil, "OVERLAY") + btn._nanamiCollapse:SetFont(GetFont(), 12, "OUTLINE") + btn._nanamiCollapse:SetPoint("LEFT", btn, "LEFT", 2, 0) + btn._nanamiCollapse:SetTextColor(T.collapseIcon[1], T.collapseIcon[2], T.collapseIcon[3]) + btn._nanamiCollapse:Hide() + + btn._nanamiTrackBar = btn:CreateTexture(nil, "OVERLAY") + btn._nanamiTrackBar:SetTexture("Interface\\Buttons\\WHITE8X8") + btn._nanamiTrackBar:SetWidth(3) + btn._nanamiTrackBar:SetPoint("TOPRIGHT", btn, "TOPRIGHT", 0, 0) + btn._nanamiTrackBar:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 0, 0) + btn._nanamiTrackBar:SetVertexColor(T.trackBar[1], T.trackBar[2], T.trackBar[3], 1) + btn._nanamiTrackBar:Hide() + + btn._nanamiTrackLabel = btn:CreateFontString(nil, "OVERLAY") + btn._nanamiTrackLabel:SetFont(GetFont(), 9, "OUTLINE") + btn._nanamiTrackLabel:SetPoint("RIGHT", btn, "RIGHT", -6, 0) + btn._nanamiTrackLabel:SetTextColor(T.trackBar[1], T.trackBar[2], T.trackBar[3], 1) + btn._nanamiTrackLabel:Hide() + + local gmFS = _G["QuestLogTitle" .. i .. "GroupMates"] + if gmFS and gmFS.SetFont then + gmFS:SetFont(GetFont(), 10, "OUTLINE") + end + + btn._nanamiSkinned = true + end + + btn:SetWidth(LIST_BTN_W) + + local normalText = _G["QuestLogTitle" .. i .. "NormalText"] + local tagFS = _G["QuestLogTitle" .. i .. "Tag"] + + if btn:IsVisible() and normalText then + normalText:SetWidth(LIST_BTN_W - 30) + + local questIndex = btn.questLogIndex + local isSelected = (btn == hlBtn) and not btn.isHeader + + if btn.isHeader then + normalText:SetTextColor(T.zoneHeader[1], T.zoneHeader[2], T.zoneHeader[3]) + if btn._nanamiCollapse then + local collapsed = false + if btn.IsExpanded then + collapsed = not btn:IsExpanded() + elseif btn.IsCollapsed then + collapsed = btn:IsCollapsed() + end + local ntex = btn.GetNormalTexture and btn:GetNormalTexture() + if ntex then + local tex = ntex.GetTexture and ntex:GetTexture() + if tex then + tex = string.lower(tex) + if string.find(tex, "plus") or string.find(tex, "expand") then + collapsed = true + elseif string.find(tex, "minus") or string.find(tex, "collapse") then + collapsed = false + end + end + ntex:SetAlpha(0) + end + btn._nanamiCollapse:SetText(collapsed and "+" or "-") + btn._nanamiCollapse:Show() + end + if btn._nanamiTrackBar then btn._nanamiTrackBar:Hide() end + if btn._nanamiTrackLabel then btn._nanamiTrackLabel:Hide() end + if tagFS then tagFS:SetText("") end + else + local diffR, diffG, diffB + if questIndex and GetQuestLogTitle then + local _, level = GetQuestLogTitle(questIndex) + if level and level > 0 then + if GetQuestDifficultyColor then + local c = GetQuestDifficultyColor(level) + if c then diffR, diffG, diffB = c.r, c.g, c.b end + end + if not diffR then + diffR, diffG, diffB = GetDiffColor(level) + end + end + end + if diffR then + normalText:SetTextColor(diffR, diffG, diffB) + elseif btn.GetTextColor then + local cr, cg, cb = btn:GetTextColor() + if cr then normalText:SetTextColor(cr, cg, cb) end + end + + if isSelected then + normalText:SetTextColor(1, 1, 1) + end + + if btn._nanamiCollapse then btn._nanamiCollapse:Hide() end + + local check = _G["QuestLogTitle" .. i .. "Check"] + local isTracked = check and check.IsVisible and check:IsVisible() + if btn._nanamiTrackBar then + if isTracked then btn._nanamiTrackBar:Show() else btn._nanamiTrackBar:Hide() end + end + if btn._nanamiTrackLabel then btn._nanamiTrackLabel:Hide() end + if check then + if isTracked then + check:SetVertexColor(T.trackBar[1], T.trackBar[2], T.trackBar[3], 1) + check:SetWidth(14) + check:SetHeight(14) + else + check:SetWidth(12) + check:SetHeight(12) + end + end + + if tagFS then + local tagText = tagFS:GetText() + if tagText and tagText ~= "" then + if isSelected then + tagFS:SetTextColor(1, 1, 1) + elseif diffR then + tagFS:SetTextColor(diffR, diffG, diffB) + elseif btn.GetTextColor then + local cr, cg, cb = btn:GetTextColor() + if cr then tagFS:SetTextColor(cr, cg, cb) end + end + end + end + end + + -- check icon is handled in the tracking section above + end + end +end + +-------------------------------------------------------------------------------- +-- Quest Detail Text Restyling +-------------------------------------------------------------------------------- +local function StyleQuestDetailText() + local titleFS = _G["QuestLogQuestTitle"] + if titleFS then + titleFS:SetFont(GetFont(), 14, "OUTLINE") + titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + end + + local descFS = _G["QuestLogQuestDescription"] + if descFS then + descFS:SetFont(GetFont(), 12) + descFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + end + + local objectivesTitle = _G["QuestLogObjectivesText"] + if objectivesTitle then + objectivesTitle:SetFont(GetFont(), 12) + objectivesTitle:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + end + + local descTitle = _G["QuestLogDescriptionTitle"] + if descTitle then + descTitle:SetFont(GetFont(), 12, "OUTLINE") + descTitle:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + end + + for i = 1, 20 do + local objFS = _G["QuestLogObjective" .. i] + if not objFS then break end + objFS:SetFont(GetFont(), 11) + local text = objFS:GetText() + if text then + local _, _, done, needed = string.find(text, "(%d+)/(%d+)") + if done and needed and tonumber(done) >= tonumber(needed) then + objFS:SetTextColor(T.objectiveComplete[1], T.objectiveComplete[2], T.objectiveComplete[3]) + else + objFS:SetTextColor(T.objectiveIncomplete[1], T.objectiveIncomplete[2], T.objectiveIncomplete[3]) + end + end + end + + local styledTexts = { + "QuestLogRewardTitleText", "QuestLogHeaderText", + "QuestLogItemChooseText", "QuestLogItemReceiveText", + "QuestLogSpellLearnText", + } + for _, name in ipairs(styledTexts) do + local fs = _G[name] + if fs then + fs:SetFont(GetFont(), 11, "OUTLINE") + fs:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + end + end + + local rewardMoney = _G["QuestLogRequiredMoneyText"] + if rewardMoney then rewardMoney:SetFont(GetFont(), 11) end + + local timerText = _G["QuestLogTimerText"] + if timerText then + timerText:SetFont(GetFont(), 11, "OUTLINE") + timerText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + end + + for i = 1, 10 do + local item = _G["QuestLogItem" .. i] + if item then SkinRewardItem(item) end + end + + ApplyQuestLogItemQuality() +end + +-------------------------------------------------------------------------------- +-- pfquest Button Detection & Styling +-------------------------------------------------------------------------------- +local pfquestSkinned = false + +local function SkinPfQuestButton(btn) + if not btn or btn._nanamiPfSkinned then return end + btn._nanamiPfSkinned = true + StripAllTextures(btn) + StripNormalTexture(btn) + StripPushedTexture(btn) + StripHighlightTexture(btn) + SetRoundBackdrop(btn, T.btnBg, T.btnBorder) + + if btn.GetFontString then + local fs = btn:GetFontString() + if fs then + fs:SetFont(GetFont(), 10, "OUTLINE") + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end + + local origEnter = btn:GetScript("OnEnter") + btn:SetScript("OnEnter", function() + SetRoundBackdrop(this, T.btnHoverBg, T.btnHoverBd) + if this.GetFontString then + local tfs = this:GetFontString() + if tfs then tfs:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end + end + if origEnter then origEnter() end + end) + local origLeave = btn:GetScript("OnLeave") + btn:SetScript("OnLeave", function() + SetRoundBackdrop(this, T.btnBg, T.btnBorder) + if this.GetFontString then + local tfs = this:GetFontString() + if tfs then tfs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end + end + if origLeave then origLeave() end + end) +end + +local function IsPfQuestButtonText(text) + if not text then return false end + return string.find(text, "show") or string.find(text, "hide") + or string.find(text, "clean") or string.find(text, "reset") + or string.find(text, "Show") or string.find(text, "Hide") + or string.find(text, "Clean") or string.find(text, "Reset") + or string.find(text, "显示") or string.find(text, "隐藏") + or string.find(text, "清空") or string.find(text, "重置") +end + +local function TrySkinPfQuestButtons() + local pfBtns = {} + + for _, name in ipairs({ + "pfQuestShow", "pfQuestHide", "pfQuestClean", "pfQuestReset", + "pfQuest.show", "pfQuest.hide", "pfQuest.clean", "pfQuest.reset", + }) do + local btn = _G[name] + if btn then + SkinPfQuestButton(btn) + table.insert(pfBtns, btn) + end + end + + if pfQuest and pfQuest.buttons then + for _, btn in pairs(pfQuest.buttons) do + if type(btn) == "table" and btn.GetObjectType then + SkinPfQuestButton(btn) + table.insert(pfBtns, btn) + end + end + end + + local detailChild = _G["QuestLogDetailScrollFrameChildFrame"] + or _G["QuestLogDetailScrollFrameChild"] + if detailChild and detailChild.GetChildren then + local children = { detailChild:GetChildren() } + for idx = 1, table.getn(children) do + local child = children[idx] + if child and child.GetObjectType and child:GetObjectType() == "Button" and child.GetFontString then + local fs = child:GetFontString() + if fs then + local text = fs:GetText() + if IsPfQuestButtonText(text) then + SkinPfQuestButton(child) + table.insert(pfBtns, child) + end + end + end + end + end + + -- Resize pfquest buttons to fit within the detail content area (keep original positions) + local contentW = FRAME_W - LIST_W - DETAIL_PAD * 2 - 4 - INNER_PAD * 2 - SCROLL_W - 6 + local btnCount = table.getn(pfBtns) + if btnCount > 0 and contentW > 100 then + local gap = 2 + local pfBtnW = math.floor((contentW - gap * (btnCount - 1)) / btnCount) + if pfBtnW < 50 then pfBtnW = 50 end + for bi = 1, btnCount do + local btn = pfBtns[bi] + if btn and btn.SetWidth then + btn:SetWidth(pfBtnW) + end + end + end +end + +-------------------------------------------------------------------------------- +-- Quest Tracker Button Skinning +-------------------------------------------------------------------------------- +local function SkinQuestTrackButton() + for _, bname in ipairs({ + "QuestLogTrack", "QuestLogFrameTrackButton", "QuestLogTrackButton", + }) do + local trackBtn = _G[bname] + if trackBtn then + trackBtn:Hide() + trackBtn:SetAlpha(0) + trackBtn:SetWidth(1) + trackBtn:SetHeight(1) + trackBtn:ClearAllPoints() + trackBtn:SetPoint("TOPLEFT", QuestLogFrame, "TOPLEFT", -9999, 0) + end + end + for _, tname in ipairs({ + "QuestLogTrackText", "QuestLogTrackButtonText", + "QuestLogTrackHighlight", + }) do + local t = _G[tname] + if t then + t:Hide() + if t.SetAlpha then t:SetAlpha(0) end + end + end +end + +-------------------------------------------------------------------------------- +-- Hooks +-------------------------------------------------------------------------------- +local function ClampHighlightWidth() + local hl = _G["QuestLogHighlightFrame"] + if hl and hl.IsVisible and hl:IsVisible() then + hl:SetWidth(LIST_BTN_W) + end +end + +local function EnforceListScrollSize() + local lf = QuestLogListScrollFrame + local f = QuestLogFrame + if not lf or not f or not f._nanamiListPanel then return end + local panelH = f._nanamiListPanel:GetHeight() + if panelH and panelH > 50 then + lf:SetHeight(panelH - INNER_PAD - 4) + end +end + +local function InstallHooks() + local origUpdate = QuestLog_Update + if origUpdate then + QuestLog_Update = function() + origUpdate() + if QuestLogFrame then QuestLogFrame:SetWidth(FRAME_W) end + EnforceListScrollSize() + StyleQuestListEntries() + ClampHighlightWidth() + if not pfquestSkinned then + pfquestSkinned = true + TrySkinPfQuestButtons() + end + end + end + + local origSetSelection = QuestLog_SetSelection + if origSetSelection then + QuestLog_SetSelection = function(index) + origSetSelection(index) + EnforceListScrollSize() + StyleQuestListEntries() + ClampHighlightWidth() + StyleQuestDetailText() + TrySkinPfQuestButtons() + if QuestLogFrame and QuestLogFrame._skinExtraButtons then + QuestLogFrame._skinExtraButtons() + end + local sb = _G["QuestLogFramePushQuestButton"] or _G["QuestFramePushQuestButton"] + if sb and sb._nanamiRefreshShare then sb._nanamiRefreshShare(sb) end + end + end + + local origUpdateDetails = QuestLog_UpdateQuestDetails + if origUpdateDetails then + QuestLog_UpdateQuestDetails = function(doNotShow) + origUpdateDetails(doNotShow) + StyleQuestDetailText() + TrySkinPfQuestButtons() + if QuestLogFrame and QuestLogFrame._skinExtraButtons then + QuestLogFrame._skinExtraButtons() + end + end + end +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function QLS:Initialize() + if skinApplied then return end + if not QuestLogFrame then return end + skinApplied = true + + SkinMainFrame() + SkinQuestList() + SkinQuestDetail() + SkinBottomButtons() + SkinQuestTrackButton() + InstallHooks() + + local origOnShow = QuestLogFrame:GetScript("OnShow") + QuestLogFrame:SetScript("OnShow", function() + if origOnShow then origOnShow() end + QuestLogFrame:SetWidth(FRAME_W) + EnforceListScrollSize() + StyleQuestListEntries() + ClampHighlightWidth() + StyleQuestDetailText() + if not pfquestSkinned then + pfquestSkinned = true + TrySkinPfQuestButtons() + end + if QuestLogFrame._skinExtraButtons then + QuestLogFrame._skinExtraButtons() + end + end) + + -- Delayed pfquest + translate button detection + local pfCheck = CreateFrame("Frame", nil, QuestLogFrame) + pfCheck._e = 0; pfCheck._n = 0 + pfCheck:SetScript("OnUpdate", function() + this._e = (this._e or 0) + (arg1 or 0) + if this._e < 0.5 then return end + this._e = 0 + this._n = (this._n or 0) + 1 + TrySkinPfQuestButtons() + if QuestLogFrame._skinExtraButtons then + QuestLogFrame._skinExtraButtons() + end + if this._n >= 8 then this:SetScript("OnUpdate", nil) end + end) + + if QuestLogFrame:IsVisible() then + StyleQuestListEntries() + StyleQuestDetailText() + end +end + +-------------------------------------------------------------------------------- +-- Bootstrap +-------------------------------------------------------------------------------- +local bootstrap = CreateFrame("Frame") +bootstrap:RegisterEvent("PLAYER_LOGIN") +bootstrap:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + if SFramesDB.enableQuestLogSkin == nil then + SFramesDB.enableQuestLogSkin = true + end + if SFramesDB.enableQuestLogSkin ~= false then + QLS:Initialize() + end + end +end) diff --git a/QuestUI.lua b/QuestUI.lua new file mode 100644 index 0000000..443f917 --- /dev/null +++ b/QuestUI.lua @@ -0,0 +1,1663 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Quest & NPC Interaction UI (QuestUI.lua) +-- Replaces GossipFrame and QuestFrame with Nanami-UI styled interface +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.QuestUI = {} +local QUI = SFrames.QuestUI +SFramesDB = SFramesDB or {} + +-------------------------------------------------------------------------------- +-- Theme: Pink Cat-Paw (matching Nanami-UI) +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + questAvail = { 0.9, 0.82, 0.18 }, + questActive = { 0.65, 0.65, 0.65 }, + questComplete = { 0.9, 0.82, 0.18 }, + moneyGold = { 1, 0.84, 0.0 }, + moneySilver = { 0.78, 0.78, 0.78 }, + moneyCopper = { 0.71, 0.43, 0.18 }, +}) + +local QUALITY_COLORS = { + [0] = { 0.62, 0.62, 0.62 }, [1] = { 1, 1, 1 }, + [2] = { 0.12, 1, 0 }, [3] = { 0.0, 0.44, 0.87 }, + [4] = { 0.64, 0.21, 0.93 }, [5] = { 1, 0.5, 0 }, +} + +local GOSSIP_ICONS = { + ["gossip"] = "Interface\\GossipFrame\\GossipGossipIcon", + ["vendor"] = "Interface\\GossipFrame\\VendorGossipIcon", + ["taxi"] = "Interface\\GossipFrame\\TaxiGossipIcon", + ["trainer"] = "Interface\\GossipFrame\\TrainerGossipIcon", + ["banker"] = "Interface\\GossipFrame\\BankerGossipIcon", + ["petition"] = "Interface\\GossipFrame\\PetitionGossipIcon", + ["tabard"] = "Interface\\GossipFrame\\TabardGossipIcon", + ["battlemaster"] = "Interface\\GossipFrame\\BattleMasterGossipIcon", +} + +local QUEST_ICON_AVAILABLE = "Interface\\GossipFrame\\AvailableQuestIcon" +local QUEST_ICON_ACTIVE = "Interface\\GossipFrame\\ActiveQuestIcon" + +local GOSSIP_TRIGGER_TEXTS = { + ["TRANSMOG_TRIGGER"] = true, +} + +-------------------------------------------------------------------------------- +-- Layout +-------------------------------------------------------------------------------- +local FRAME_W = 340 +local FRAME_H = 400 +local HEADER_H = 34 +local BOTTOM_H = 42 +local SIDE_PAD = 14 +local CONTENT_W = FRAME_W - SIDE_PAD * 2 +local ITEM_SIZE = 36 +local ITEM_GAP = 4 +local SCROLL_STEP = 40 +local MAX_OPTIONS = 24 +local MAX_ITEMS = 10 + +-------------------------------------------------------------------------------- +-- State +-------------------------------------------------------------------------------- +local MainFrame = nil +local currentPage = nil +local previousPage = nil +local selectedReward = 0 +local pendingClose = false +local closeTimer = 0 +local pages = {} + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) +end + +local function CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.45) + s:SetBackdropBorderColor(0, 0, 0, 0.6) + s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) + return s +end + +local function MakeSep(parent, width) + local sep = parent:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetWidth(width or CONTENT_W) + sep:SetHeight(1) + sep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + return sep +end + +local function GetDiffColor(level) + if GetDifficultyColor then + local c = GetDifficultyColor(level) + if c then return c.r, c.g, c.b end + end + local pLvl = UnitLevel("player") or 60 + local diff = (level or pLvl) - pLvl + if diff >= 5 then return 1, 0.1, 0.1 + elseif diff >= 3 then return 1, 0.5, 0.25 + elseif diff >= -2 then return 1, 1, 0 + else return 0.25, 0.75, 0.25 + end +end + +local function TextHeight(fs, fallback) + if not fs then return fallback or 14 end + local h = fs.GetStringHeight and fs:GetStringHeight() + if h and h > 0 then return h end + h = fs:GetHeight() + if h and h > 1 then return h end + return fallback or 14 +end + +local function ForwardScrollWheel(frame) + frame:EnableMouseWheel(true) + frame:SetScript("OnMouseWheel", function() + local p = this:GetParent() + while p do + if p:GetObjectType() == "ScrollFrame" then + local cur = p:GetVerticalScroll() + local maxVal = p:GetVerticalScrollRange() + if arg1 > 0 then + p:SetVerticalScroll(math.max(0, cur - SCROLL_STEP)) + else + p:SetVerticalScroll(math.min(maxVal, cur + SCROLL_STEP)) + end + return + end + p = p:GetParent() + end + end) +end + +local function FormatMoney(copper) + if not copper or copper <= 0 then return nil end + local g = math.floor(copper / 10000) + local s = math.floor(math.mod(copper, 10000) / 100) + local c = math.mod(copper, 100) + return g, s, c +end + +local LINK_QUALITY_MAP = { + ["9d9d9d"] = 0, ["ffffff"] = 1, ["1eff00"] = 2, + ["0070dd"] = 3, ["a335ee"] = 4, ["ff8000"] = 5, + ["e6cc80"] = 6, +} + +local function QualityFromLink(link) + if not link then return nil end + local _, _, hex = string.find(link, "|c(%x%x%x%x%x%x%x%x)|") + if hex and string.len(hex) == 8 then + local color = string.lower(string.sub(hex, 3, 8)) + return LINK_QUALITY_MAP[color] + end + return nil +end + +local function GetItemQuality(itemType, index) + local name, texture, numItems, quality, isUsable = GetQuestItemInfo(itemType, index) + if quality and quality > 0 then return quality end + local link + if GetQuestItemLink then + link = GetQuestItemLink(itemType, index) + end + if link then + if GetItemInfo then + local _, _, q = GetItemInfo(link) + if q and q > 0 then return q end + end + local q = QualityFromLink(link) + if q then return q end + end + return nil +end +-------------------------------------------------------------------------------- +-- Scroll Area Factory +-------------------------------------------------------------------------------- +local function CreateScrollArea(parent, name) + local scroll = CreateFrame("ScrollFrame", name, parent) + local content = CreateFrame("Frame", name .. "Content", scroll) + content:SetWidth(CONTENT_W) + content:SetHeight(1) + scroll:SetScrollChild(content) + + scroll:EnableMouseWheel(true) + scroll:SetScript("OnMouseWheel", function() + local cur = this:GetVerticalScroll() + local maxVal = this:GetVerticalScrollRange() + if arg1 > 0 then + this:SetVerticalScroll(math.max(0, cur - SCROLL_STEP)) + else + this:SetVerticalScroll(math.min(maxVal, cur + SCROLL_STEP)) + end + end) + + scroll.content = content + return scroll +end + +-------------------------------------------------------------------------------- +-- Action Button Factory +-------------------------------------------------------------------------------- +local function CreateActionBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 100) + btn:SetHeight(28) + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 11, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + fs:SetText(text) + btn.label = fs + + btn.disabled = false + function btn:SetDisabled(flag) + self.disabled = flag + if flag then + self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) + self:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], 0.5) + else + self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + end + end + + btn:SetScript("OnEnter", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end + end) + btn:SetScript("OnLeave", function() + if not this.disabled then + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end) + btn:SetScript("OnMouseDown", function() + if not this.disabled then + this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) + end + end) + btn:SetScript("OnMouseUp", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + end + end) + + return btn +end + +-------------------------------------------------------------------------------- +-- Gossip / Quest Option Button Factory +-------------------------------------------------------------------------------- +local function CreateOptionButton(parent) + local btn = CreateFrame("Button", nil, parent) + btn:SetHeight(22) + btn:SetWidth(CONTENT_W) + + local icon = btn:CreateTexture(nil, "ARTWORK") + icon:SetWidth(16) + icon:SetHeight(16) + icon:SetPoint("LEFT", btn, "LEFT", 2, 0) + btn.icon = icon + + local text = btn:CreateFontString(nil, "OVERLAY") + text:SetFont(GetFont(), 12) + text:SetPoint("LEFT", icon, "RIGHT", 6, 0) + text:SetPoint("RIGHT", btn, "RIGHT", -4, 0) + text:SetJustifyH("LEFT") + text:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + btn.text = text + + btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD") + + btn.normalR = T.nameText[1] + btn.normalG = T.nameText[2] + btn.normalB = T.nameText[3] + + btn:SetScript("OnEnter", function() + this.text:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + end) + btn:SetScript("OnLeave", function() + this.text:SetTextColor(this.normalR, this.normalG, this.normalB) + end) + + ForwardScrollWheel(btn) + return btn +end +-------------------------------------------------------------------------------- +-- Item Slot Factory +-------------------------------------------------------------------------------- +local function CreateItemSlot(parent) + local slot = CreateFrame("Button", nil, parent) + slot:SetWidth(CONTENT_W) + slot:SetHeight(ITEM_SIZE + 4) + + -- Selection highlight background (covers entire row) + local selBg = slot:CreateTexture(nil, "BACKGROUND") + selBg:SetTexture("Interface\\Buttons\\WHITE8X8") + selBg:SetAllPoints(slot) + selBg:SetVertexColor(T.questSelected[1], T.questSelected[2], T.questSelected[3], T.questSelected[4] or 0.45) + selBg:Hide() + slot.selBg = selBg + + -- Selection glow border around entire row + local selGlow = CreateFrame("Frame", nil, slot) + selGlow:SetPoint("TOPLEFT", slot, "TOPLEFT", -2, 2) + selGlow:SetPoint("BOTTOMRIGHT", slot, "BOTTOMRIGHT", 2, -2) + selGlow:SetBackdrop({ + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + selGlow:SetBackdropBorderColor(T.questSelBorder[1], T.questSelBorder[2], T.questSelBorder[3], T.questSelBorder[4] or 0.9) + selGlow:Hide() + slot.selGlow = selGlow + + local iconFrame = CreateFrame("Frame", nil, slot) + iconFrame:SetWidth(ITEM_SIZE) + iconFrame:SetHeight(ITEM_SIZE) + iconFrame:SetPoint("LEFT", slot, "LEFT", 0, 0) + iconFrame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + slot.iconFrame = iconFrame + + local icon = iconFrame:CreateTexture(nil, "ARTWORK") + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 3, -3) + icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -3, 3) + slot.icon = icon + + local qualGlow = iconFrame:CreateTexture(nil, "OVERLAY") + qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + qualGlow:SetBlendMode("ADD") + qualGlow:SetAlpha(0.7) + qualGlow:SetWidth(ITEM_SIZE * 1.8) + qualGlow:SetHeight(ITEM_SIZE * 1.8) + qualGlow:SetPoint("CENTER", iconFrame, "CENTER", 0, 0) + qualGlow:Hide() + slot.qualGlow = qualGlow + + local countFS = iconFrame:CreateFontString(nil, "OVERLAY") + countFS:SetFont("Fonts\\ARIALN.TTF", 11, "OUTLINE") + countFS:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -2, 2) + countFS:SetJustifyH("RIGHT") + slot.countFS = countFS + + local nameFS = slot:CreateFontString(nil, "OVERLAY") + nameFS:SetFont(GetFont(), 12, "OUTLINE") + nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 8, 0) + nameFS:SetPoint("RIGHT", slot, "RIGHT", -4, 0) + nameFS:SetJustifyH("LEFT") + nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + slot.nameFS = nameFS + + slot.itemType = nil + slot.itemIndex = nil + + slot:SetScript("OnEnter", function() + if this.itemType and this.itemIndex then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetQuestItem(this.itemType, this.itemIndex) + GameTooltip:Show() + end + this.iconFrame:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4]) + end) + slot:SetScript("OnLeave", function() + GameTooltip:Hide() + if this.selected then + this.iconFrame:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], T.slotSelected[4]) + else + local qc = QUALITY_COLORS[this._quality] + if qc and this._quality and this._quality >= 2 then + this.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1) + else + this.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + end + end + end) + + function slot:ApplyItemData(name, texture, numItems, quality) + self.icon:SetTexture(texture) + self.nameFS:SetText(name or "") + if numItems and numItems > 1 then + self.countFS:SetText(numItems) + else + self.countFS:SetText("") + end + self._quality = quality + local qc = QUALITY_COLORS[quality] + if qc and quality and quality >= 2 then + self.qualGlow:SetVertexColor(qc[1], qc[2], qc[3]) + self.qualGlow:Show() + self.nameFS:SetTextColor(qc[1], qc[2], qc[3]) + self.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1) + else + self.qualGlow:Hide() + self.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + end + end + + function slot:SetItemInfo(iType, idx) + self.itemType = iType + self.itemIndex = idx + local name, texture, numItems, quality, isUsable = GetQuestItemInfo(iType, idx) + if not quality or quality <= 0 then + quality = GetItemQuality(iType, idx) + end + self:ApplyItemData(name, texture, numItems, quality or 1) + self:Show() + + self:SetScript("OnUpdate", nil) + local needRetry = (not name or name == "") or (quality == nil) or (not texture or texture == "") + if needRetry then + self._retryElapsed = 0 + self._retryAttempts = 0 + self:SetScript("OnUpdate", function() + this._retryElapsed = (this._retryElapsed or 0) + (arg1 or 0) + if this._retryElapsed < 0.2 then return end + this._retryElapsed = 0 + this._retryAttempts = (this._retryAttempts or 0) + 1 + if this._retryAttempts > 15 then + this:SetScript("OnUpdate", nil) + return + end + if this.itemType and this.itemIndex then + local n, t, ni, q = GetQuestItemInfo(this.itemType, this.itemIndex) + if not q or q <= 0 then q = GetItemQuality(this.itemType, this.itemIndex) end + local nameOk = n and n ~= "" + local texOk = t and t ~= "" + local qualOk = q ~= nil + if nameOk and texOk and qualOk then + this:ApplyItemData(n, t, ni, q) + this:SetScript("OnUpdate", nil) + elseif this._retryAttempts >= 15 then + if nameOk then + this:ApplyItemData(n, t, ni, q or 1) + end + this:SetScript("OnUpdate", nil) + end + end + end) + end + end + + function slot:SetSelected(flag) + self.selected = flag + if flag then + self.iconFrame:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], T.slotSelected[4]) + self.iconFrame:SetBackdropColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], T.slotSelected[4] or 0.95) + self.selBg:Show() + self.selGlow:Show() + self.nameFS:SetTextColor(T.title[1], T.title[2], T.title[3]) + else + local qc = QUALITY_COLORS[self._quality] + if qc and self._quality and self._quality >= 2 then + self.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1) + self.nameFS:SetTextColor(qc[1], qc[2], qc[3]) + else + self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + self.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + end + self.iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + self.selBg:Hide() + self.selGlow:Hide() + end + end + + function slot:Clear() + self:SetScript("OnUpdate", nil) + self.itemType = nil + self.itemIndex = nil + self._quality = nil + self.icon:SetTexture(nil) + self.nameFS:SetText("") + self.countFS:SetText("") + self.qualGlow:Hide() + self.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + self.selected = false + self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + self.iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + self.selBg:Hide() + self.selGlow:Hide() + self:Hide() + end + + ForwardScrollWheel(slot) + return slot +end +-------------------------------------------------------------------------------- +-- Money Display +-------------------------------------------------------------------------------- +local function CreateMoneyLine(parent) + local frame = CreateFrame("Frame", nil, parent) + frame:SetWidth(CONTENT_W) + frame:SetHeight(16) + + local font = GetFont() + + local gTxt = frame:CreateFontString(nil, "OVERLAY") + gTxt:SetFont(font, 12, "OUTLINE") + local gTex = frame:CreateTexture(nil, "ARTWORK") + gTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") + gTex:SetTexCoord(0, 0.25, 0, 1) + gTex:SetWidth(13); gTex:SetHeight(13) + + local sTxt = frame:CreateFontString(nil, "OVERLAY") + sTxt:SetFont(font, 12, "OUTLINE") + local sTex = frame:CreateTexture(nil, "ARTWORK") + sTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") + sTex:SetTexCoord(0.25, 0.5, 0, 1) + sTex:SetWidth(13); sTex:SetHeight(13) + + local cTxt = frame:CreateFontString(nil, "OVERLAY") + cTxt:SetFont(font, 12, "OUTLINE") + local cTex = frame:CreateTexture(nil, "ARTWORK") + cTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") + cTex:SetTexCoord(0.5, 0.75, 0, 1) + cTex:SetWidth(13); cTex:SetHeight(13) + + frame.gTxt = gTxt; frame.gTex = gTex + frame.sTxt = sTxt; frame.sTex = sTex + frame.cTxt = cTxt; frame.cTex = cTex + + function frame:SetMoney(copper) + local g, s, c = FormatMoney(copper) + if not g then self:Hide(); return end + local x = 0 + if g > 0 then + self.gTxt:SetText(g) + self.gTxt:SetTextColor(T.moneyGold[1], T.moneyGold[2], T.moneyGold[3]) + self.gTxt:ClearAllPoints() + self.gTxt:SetPoint("LEFT", self, "LEFT", x, 0) + x = x + self.gTxt:GetStringWidth() + 1 + self.gTex:ClearAllPoints() + self.gTex:SetPoint("LEFT", self, "LEFT", x, 0) + x = x + 15 + self.gTxt:Show(); self.gTex:Show() + else + self.gTxt:Hide(); self.gTex:Hide() + end + if s > 0 or g > 0 then + self.sTxt:SetText(s) + self.sTxt:SetTextColor(T.moneySilver[1], T.moneySilver[2], T.moneySilver[3]) + self.sTxt:ClearAllPoints() + self.sTxt:SetPoint("LEFT", self, "LEFT", x, 0) + x = x + self.sTxt:GetStringWidth() + 1 + self.sTex:ClearAllPoints() + self.sTex:SetPoint("LEFT", self, "LEFT", x, 0) + x = x + 15 + self.sTxt:Show(); self.sTex:Show() + else + self.sTxt:Hide(); self.sTex:Hide() + end + if c > 0 or (g == 0 and s == 0) then + self.cTxt:SetText(c) + self.cTxt:SetTextColor(T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3]) + self.cTxt:ClearAllPoints() + self.cTxt:SetPoint("LEFT", self, "LEFT", x, 0) + x = x + self.cTxt:GetStringWidth() + 1 + self.cTex:ClearAllPoints() + self.cTex:SetPoint("LEFT", self, "LEFT", x, 0) + self.cTxt:Show(); self.cTex:Show() + else + self.cTxt:Hide(); self.cTex:Hide() + end + self:Show() + end + + return frame +end + +-------------------------------------------------------------------------------- +-- Gossip data parsers +-------------------------------------------------------------------------------- +local function ParseGossipAvailableQuests() + local data = { GetGossipAvailableQuests() } + local out = {} + local i = 1 + while i <= table.getn(data) do + if type(data[i]) == "string" then + table.insert(out, { title = data[i], level = tonumber(data[i + 1]) or 0 }) + i = i + 2 + else + i = i + 1 + end + end + return out +end + +local function ParseGossipActiveQuests() + local data = { GetGossipActiveQuests() } + local out = {} + local i = 1 + while i <= table.getn(data) do + if type(data[i]) == "string" then + local q = { title = data[i], level = tonumber(data[i + 1]) or 0 } + if i + 2 <= table.getn(data) and type(data[i + 2]) ~= "string" then + q.isComplete = data[i + 2] + i = i + 3 + else + i = i + 2 + end + table.insert(out, q) + else + i = i + 1 + end + end + return out +end + +local function ParseGossipOptions() + local data = { GetGossipOptions() } + local out = {} + local i = 1 + while i <= table.getn(data) do + if i + 1 <= table.getn(data) then + table.insert(out, { title = data[i], gtype = data[i + 1] or "gossip" }) + end + i = i + 2 + end + return out +end + +-------------------------------------------------------------------------------- +-- AutoGossip logic (integrated from AutoGossip.lua) +-------------------------------------------------------------------------------- +local function TryAutoGossip() + if SFramesDB and SFramesDB.autoGossip == false then return false end + if IsShiftKeyDown() then return false end + + local active = { GetGossipActiveQuests() } + local avail = { GetGossipAvailableQuests() } + local opts = { GetGossipOptions() } + + local nActive = table.getn(active) or 0 + local nAvail = table.getn(avail) or 0 + local nOpts = table.getn(opts) or 0 + + if nActive == 0 and nAvail == 0 and nOpts == 2 then + SelectGossipOption(1) + return true + end + return false +end +-------------------------------------------------------------------------------- +-- Page Management +-------------------------------------------------------------------------------- +local function HideAllPages() + for _, p in pairs(pages) do + if p and p.Hide then p:Hide() end + end +end + +local function ShowPage(name) + HideAllPages() + currentPage = name + if pages[name] then + pages[name]:Show() + if pages[name].scroll then + pages[name].scroll:SetVerticalScroll(0) + end + end + if MainFrame then + MainFrame.btn1:Hide() + MainFrame.btn2:Hide() + end +end + +local function GoBack() + if DeclineQuest then DeclineQuest() end +end + +local function CloseFrame() + currentPage = nil + MainFrame:Hide() +end + +local function UpdateBottomButtons() + if not MainFrame then return end + local bL = MainFrame.btn1 + local bR = MainFrame.btn2 + local hasBack = previousPage and (currentPage ~= "gossip") and (currentPage ~= "greeting") + + if currentPage == "gossip" or currentPage == "greeting" then + bL:Hide() + bR.label:SetText("关闭") + bR:SetDisabled(false) + bR:SetScript("OnClick", function() + if currentPage == "gossip" then + if CloseGossip then CloseGossip() end + end + currentPage = nil + MainFrame:Hide() + end) + bR:Show() + + elseif currentPage == "detail" then + bL.label:SetText("接受") + bL:SetDisabled(false) + bL:SetScript("OnClick", function() + if AcceptQuest then AcceptQuest() end + end) + bL:Show() + + bR.label:SetText(hasBack and "返回" or "拒绝") + bR:SetDisabled(false) + bR:SetScript("OnClick", function() + if hasBack then + GoBack() + else + if DeclineQuest then DeclineQuest() end + end + end) + bR:Show() + + elseif currentPage == "progress" then + local canComplete = IsQuestCompletable and IsQuestCompletable() + bL.label:SetText("继续") + bL:SetDisabled(not canComplete) + bL:SetScript("OnClick", function() + if not this.disabled and CompleteQuest then + CompleteQuest() + end + end) + bL:Show() + + bR.label:SetText(hasBack and "返回" or "关闭") + bR:SetDisabled(false) + bR:SetScript("OnClick", function() + if hasBack then + GoBack() + else + CloseFrame() + end + end) + bR:Show() + + elseif currentPage == "complete" then + local numChoices = GetNumQuestChoices and GetNumQuestChoices() or 0 + local canFinish = (numChoices == 0) or (selectedReward > 0) + bL.label:SetText("完成任务") + bL:SetDisabled(not canFinish) + bL:SetScript("OnClick", function() + if this.disabled then return end + if GetQuestReward then + local nc = GetNumQuestChoices and GetNumQuestChoices() or 0 + if nc > 0 then + GetQuestReward(selectedReward) + else + GetQuestReward() + end + end + end) + bL:Show() + + bR.label:SetText(hasBack and "返回" or "关闭") + bR:SetDisabled(false) + bR:SetScript("OnClick", function() + if hasBack then + GoBack() + else + CloseFrame() + end + end) + bR:Show() + end +end +-------------------------------------------------------------------------------- +-- GOSSIP PAGE +-------------------------------------------------------------------------------- +local gossipOptionBtns = {} +local gossipSectionLabels = {} + +local function CreateGossipPage(parent) + local page = CreateFrame("Frame", nil, parent) + page:SetAllPoints(parent) + page:Hide() + + local scroll = CreateScrollArea(page, "SFramesQuestGossipScroll") + scroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + scroll:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) + page.scroll = scroll + + local content = scroll.content + + local npcText = content:CreateFontString(nil, "OVERLAY") + npcText:SetFont(GetFont(), 12) + npcText:SetWidth(CONTENT_W - 4) + npcText:SetJustifyH("LEFT") + npcText:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + page.npcText = npcText + + for i = 1, 3 do + local lbl = content:CreateFontString(nil, "OVERLAY") + lbl:SetFont(GetFont(), 11, "OUTLINE") + lbl:SetWidth(CONTENT_W) + lbl:SetJustifyH("LEFT") + lbl:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + gossipSectionLabels[i] = lbl + end + + for i = 1, MAX_OPTIONS do + gossipOptionBtns[i] = CreateOptionButton(content) + end + + page.Update = function() + local y = 6 + local gossipText = GetGossipText and GetGossipText() or "" + page.npcText:SetText(gossipText) + page.npcText:ClearAllPoints() + page.npcText:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + y = y + TextHeight(page.npcText, 14) + 12 + + local availQuests = ParseGossipAvailableQuests() + local activeQuests = ParseGossipActiveQuests() + local gossipOpts = ParseGossipOptions() + + local btnIdx = 1 + for i = 1, 3 do gossipSectionLabels[i]:Hide() end + for i = 1, MAX_OPTIONS do gossipOptionBtns[i]:Hide() end + + if table.getn(availQuests) > 0 then + gossipSectionLabels[1]:SetText("可接任务") + gossipSectionLabels[1]:ClearAllPoints() + gossipSectionLabels[1]:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + gossipSectionLabels[1]:Show() + y = y + 16 + + for qi = 1, table.getn(availQuests) do + local q = availQuests[qi] + if btnIdx <= MAX_OPTIONS then + local btn = gossipOptionBtns[btnIdx] + btn.icon:SetTexture(QUEST_ICON_AVAILABLE) + local levelStr = q.level > 0 and ("[" .. q.level .. "] ") or "" + btn.text:SetText(levelStr .. (q.title or "")) + if q.level > 0 then + local r, g, b = GetDiffColor(q.level) + btn.normalR = r; btn.normalG = g; btn.normalB = b + btn.text:SetTextColor(r, g, b) + else + btn.normalR = T.questAvail[1]; btn.normalG = T.questAvail[2]; btn.normalB = T.questAvail[3] + btn.text:SetTextColor(T.questAvail[1], T.questAvail[2], T.questAvail[3]) + end + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + local idx = qi + btn:SetScript("OnClick", function() + currentPage = nil + MainFrame:Hide() + SelectGossipAvailableQuest(idx) + end) + btn:Show() + y = y + 22 + btnIdx = btnIdx + 1 + end + end + y = y + 4 + end + + if table.getn(activeQuests) > 0 then + gossipSectionLabels[2]:SetText("进行中任务") + gossipSectionLabels[2]:ClearAllPoints() + gossipSectionLabels[2]:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + gossipSectionLabels[2]:Show() + y = y + 16 + + for qi = 1, table.getn(activeQuests) do + local q = activeQuests[qi] + if btnIdx <= MAX_OPTIONS then + local btn = gossipOptionBtns[btnIdx] + btn.icon:SetTexture(QUEST_ICON_ACTIVE) + local levelStr = q.level > 0 and ("[" .. q.level .. "] ") or "" + btn.text:SetText(levelStr .. (q.title or "")) + if q.isComplete then + btn.normalR = T.questComplete[1]; btn.normalG = T.questComplete[2]; btn.normalB = T.questComplete[3] + btn.text:SetTextColor(T.questComplete[1], T.questComplete[2], T.questComplete[3]) + else + btn.normalR = T.questActive[1]; btn.normalG = T.questActive[2]; btn.normalB = T.questActive[3] + btn.text:SetTextColor(T.questActive[1], T.questActive[2], T.questActive[3]) + end + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + local idx = qi + btn:SetScript("OnClick", function() + currentPage = nil + MainFrame:Hide() + SelectGossipActiveQuest(idx) + end) + btn:Show() + y = y + 22 + btnIdx = btnIdx + 1 + end + end + y = y + 4 + end + + if table.getn(gossipOpts) > 0 then + gossipSectionLabels[3]:SetText("对话") + gossipSectionLabels[3]:ClearAllPoints() + gossipSectionLabels[3]:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + gossipSectionLabels[3]:Show() + y = y + 16 + + for oi = 1, table.getn(gossipOpts) do + local opt = gossipOpts[oi] + if btnIdx <= MAX_OPTIONS then + local btn = gossipOptionBtns[btnIdx] + btn.icon:SetTexture(GOSSIP_ICONS[opt.gtype] or GOSSIP_ICONS["gossip"]) + btn.text:SetText(opt.title or "") + btn.normalR = T.nameText[1]; btn.normalG = T.nameText[2]; btn.normalB = T.nameText[3] + btn.text:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + local idx = oi + btn:SetScript("OnClick", function() + currentPage = nil + MainFrame:Hide() + SelectGossipOption(idx) + end) + btn:Show() + y = y + 22 + btnIdx = btnIdx + 1 + end + end + end + + content:SetHeight(y + 10) + end + + return page +end +-------------------------------------------------------------------------------- +-- GREETING PAGE (multi-quest NPC via QuestFrame) +-------------------------------------------------------------------------------- +local greetingOptionBtns = {} +local greetingSectionLabels = {} + +local function CreateGreetingPage(parent) + local page = CreateFrame("Frame", nil, parent) + page:SetAllPoints(parent) + page:Hide() + + local scroll = CreateScrollArea(page, "SFramesQuestGreetingScroll") + scroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + scroll:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) + page.scroll = scroll + + local content = scroll.content + + local npcText = content:CreateFontString(nil, "OVERLAY") + npcText:SetFont(GetFont(), 12) + npcText:SetWidth(CONTENT_W - 4) + npcText:SetJustifyH("LEFT") + npcText:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + page.npcText = npcText + + for i = 1, 2 do + local lbl = content:CreateFontString(nil, "OVERLAY") + lbl:SetFont(GetFont(), 11, "OUTLINE") + lbl:SetWidth(CONTENT_W) + lbl:SetJustifyH("LEFT") + lbl:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + greetingSectionLabels[i] = lbl + end + + for i = 1, MAX_OPTIONS do + greetingOptionBtns[i] = CreateOptionButton(content) + end + + page.Update = function() + local y = 6 + local greeting = GetGreetingText and GetGreetingText() or "" + page.npcText:SetText(greeting) + page.npcText:ClearAllPoints() + page.npcText:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + y = y + TextHeight(page.npcText, 14) + 12 + + local numAvail = GetNumAvailableQuests and GetNumAvailableQuests() or 0 + local numActive = GetNumActiveQuests and GetNumActiveQuests() or 0 + + local btnIdx = 1 + for i = 1, 2 do greetingSectionLabels[i]:Hide() end + for i = 1, MAX_OPTIONS do greetingOptionBtns[i]:Hide() end + + if numAvail > 0 then + greetingSectionLabels[1]:SetText("可接任务") + greetingSectionLabels[1]:ClearAllPoints() + greetingSectionLabels[1]:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + greetingSectionLabels[1]:Show() + y = y + 16 + + for qi = 1, numAvail do + if btnIdx <= MAX_OPTIONS then + local title = GetAvailableTitle and GetAvailableTitle(qi) or ("Quest " .. qi) + local btn = greetingOptionBtns[btnIdx] + btn.icon:SetTexture(QUEST_ICON_AVAILABLE) + btn.text:SetText(title) + btn.normalR = T.questAvail[1]; btn.normalG = T.questAvail[2]; btn.normalB = T.questAvail[3] + btn.text:SetTextColor(T.questAvail[1], T.questAvail[2], T.questAvail[3]) + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + local idx = qi + btn:SetScript("OnClick", function() + currentPage = nil + MainFrame:Hide() + SelectAvailableQuest(idx) + end) + btn:Show() + y = y + 22 + btnIdx = btnIdx + 1 + end + end + y = y + 4 + end + + if numActive > 0 then + greetingSectionLabels[2]:SetText("进行中任务") + greetingSectionLabels[2]:ClearAllPoints() + greetingSectionLabels[2]:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + greetingSectionLabels[2]:Show() + y = y + 16 + + for qi = 1, numActive do + if btnIdx <= MAX_OPTIONS then + local title = GetActiveTitle and GetActiveTitle(qi) or ("Quest " .. qi) + local btn = greetingOptionBtns[btnIdx] + btn.icon:SetTexture(QUEST_ICON_ACTIVE) + btn.text:SetText(title) + btn.normalR = T.questActive[1]; btn.normalG = T.questActive[2]; btn.normalB = T.questActive[3] + btn.text:SetTextColor(T.questActive[1], T.questActive[2], T.questActive[3]) + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + local idx = qi + btn:SetScript("OnClick", function() + currentPage = nil + MainFrame:Hide() + SelectActiveQuest(idx) + end) + btn:Show() + y = y + 22 + btnIdx = btnIdx + 1 + end + end + end + + content:SetHeight(y + 10) + end + + return page +end +-------------------------------------------------------------------------------- +-- DETAIL PAGE (accept / decline quest) +-------------------------------------------------------------------------------- +local detailItemSlots = {} + +local function CreateDetailPage(parent) + local page = CreateFrame("Frame", nil, parent) + page:SetAllPoints(parent) + page:Hide() + + local scroll = CreateScrollArea(page, "SFramesQuestDetailScroll") + scroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + scroll:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) + page.scroll = scroll + local content = scroll.content + + page.titleFS = content:CreateFontString(nil, "OVERLAY") + page.titleFS:SetFont(GetFont(), 14, "OUTLINE") + page.titleFS:SetWidth(CONTENT_W - 4) + page.titleFS:SetJustifyH("LEFT") + page.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + + page.sepLine = MakeSep(content, CONTENT_W) + + page.descFS = content:CreateFontString(nil, "OVERLAY") + page.descFS:SetFont(GetFont(), 12) + page.descFS:SetWidth(CONTENT_W - 4) + page.descFS:SetJustifyH("LEFT") + page.descFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + + page.objLabel = content:CreateFontString(nil, "OVERLAY") + page.objLabel:SetFont(GetFont(), 12, "OUTLINE") + page.objLabel:SetWidth(CONTENT_W) + page.objLabel:SetJustifyH("LEFT") + page.objLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + page.objLabel:SetText("任务目标") + + page.objFS = content:CreateFontString(nil, "OVERLAY") + page.objFS:SetFont(GetFont(), 12) + page.objFS:SetWidth(CONTENT_W - 4) + page.objFS:SetJustifyH("LEFT") + page.objFS:SetTextColor(T.objectiveText[1], T.objectiveText[2], T.objectiveText[3]) + + page.reqLabel = content:CreateFontString(nil, "OVERLAY") + page.reqLabel:SetFont(GetFont(), 11, "OUTLINE") + page.reqLabel:SetWidth(CONTENT_W) + page.reqLabel:SetJustifyH("LEFT") + page.reqLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + page.reqLabel:SetText("所需物品") + + for i = 1, MAX_ITEMS do detailItemSlots[i] = CreateItemSlot(content) end + + page.rewardLabel = content:CreateFontString(nil, "OVERLAY") + page.rewardLabel:SetFont(GetFont(), 11, "OUTLINE") + page.rewardLabel:SetWidth(CONTENT_W) + page.rewardLabel:SetJustifyH("LEFT") + page.rewardLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + page.rewardLabel:SetText("任务奖励") + + page.detailRewardSlots = {} + for i = 1, MAX_ITEMS do page.detailRewardSlots[i] = CreateItemSlot(content) end + + page.detailChoiceLabel = content:CreateFontString(nil, "OVERLAY") + page.detailChoiceLabel:SetFont(GetFont(), 11, "OUTLINE") + page.detailChoiceLabel:SetWidth(CONTENT_W) + page.detailChoiceLabel:SetJustifyH("LEFT") + page.detailChoiceLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + page.detailChoiceLabel:SetText("可选奖励") + + page.detailChoiceSlots = {} + for i = 1, MAX_ITEMS do page.detailChoiceSlots[i] = CreateItemSlot(content) end + + page.moneyLine = CreateMoneyLine(content) + + page.Update = function() + local y = 6 + page.titleFS:SetText(GetTitleText and GetTitleText() or "") + page.titleFS:ClearAllPoints() + page.titleFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + y = y + TextHeight(page.titleFS, 16) + 8 + + page.sepLine:ClearAllPoints() + page.sepLine:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + page.sepLine:Show() + y = y + 6 + + page.descFS:SetText(GetQuestText and GetQuestText() or "") + page.descFS:ClearAllPoints() + page.descFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + y = y + TextHeight(page.descFS, 14) + 12 + + local obj = GetObjectiveText and GetObjectiveText() or "" + if obj ~= "" then + page.objLabel:ClearAllPoints() + page.objLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + page.objLabel:Show(); y = y + 16 + page.objFS:SetText(obj) + page.objFS:ClearAllPoints() + page.objFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + page.objFS:Show() + y = y + TextHeight(page.objFS, 14) + 10 + else + page.objLabel:Hide(); page.objFS:Hide() + end + + local numReq = GetNumQuestItems and GetNumQuestItems() or 0 + for i = 1, MAX_ITEMS do detailItemSlots[i]:Clear() end + if numReq > 0 then + page.reqLabel:ClearAllPoints() + page.reqLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + page.reqLabel:Show(); y = y + 16 + for i = 1, math.min(numReq, MAX_ITEMS) do + detailItemSlots[i]:ClearAllPoints() + detailItemSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y) + detailItemSlots[i]:SetItemInfo("required", i) + y = y + ITEM_SIZE + ITEM_GAP + end + y = y + 4 + else + page.reqLabel:Hide() + end + + local numRew = GetNumQuestRewards and GetNumQuestRewards() or 0 + local numCho = GetNumQuestChoices and GetNumQuestChoices() or 0 + local money = GetRewardMoney and GetRewardMoney() or 0 + + for i = 1, MAX_ITEMS do page.detailRewardSlots[i]:Clear(); page.detailChoiceSlots[i]:Clear() end + page.rewardLabel:Hide(); page.detailChoiceLabel:Hide(); page.moneyLine:Hide() + + if numRew > 0 or money > 0 then + page.rewardLabel:ClearAllPoints() + page.rewardLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + page.rewardLabel:Show(); y = y + 16 + for i = 1, math.min(numRew, MAX_ITEMS) do + page.detailRewardSlots[i]:ClearAllPoints() + page.detailRewardSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y) + page.detailRewardSlots[i]:SetItemInfo("reward", i) + y = y + ITEM_SIZE + ITEM_GAP + end + if money > 0 then + page.moneyLine:ClearAllPoints() + page.moneyLine:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y) + page.moneyLine:SetMoney(money); y = y + 20 + end + y = y + 4 + end + + if numCho > 0 then + page.detailChoiceLabel:ClearAllPoints() + page.detailChoiceLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + page.detailChoiceLabel:Show(); y = y + 16 + for i = 1, math.min(numCho, MAX_ITEMS) do + page.detailChoiceSlots[i]:ClearAllPoints() + page.detailChoiceSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y) + page.detailChoiceSlots[i]:SetItemInfo("choice", i) + y = y + ITEM_SIZE + ITEM_GAP + end + y = y + 4 + end + content:SetHeight(y + 10) + end + return page +end +-------------------------------------------------------------------------------- +-- PROGRESS PAGE (turn-in requirements) +-------------------------------------------------------------------------------- +local progressItemSlots = {} + +local function CreateProgressPage(parent) + local page = CreateFrame("Frame", nil, parent) + page:SetAllPoints(parent) + page:Hide() + + local scroll = CreateScrollArea(page, "SFramesQuestProgressScroll") + scroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + scroll:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) + page.scroll = scroll + local content = scroll.content + + page.titleFS = content:CreateFontString(nil, "OVERLAY") + page.titleFS:SetFont(GetFont(), 14, "OUTLINE") + page.titleFS:SetWidth(CONTENT_W - 4) + page.titleFS:SetJustifyH("LEFT") + page.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + + page.sepLine = MakeSep(content, CONTENT_W) + + page.progressFS = content:CreateFontString(nil, "OVERLAY") + page.progressFS:SetFont(GetFont(), 12) + page.progressFS:SetWidth(CONTENT_W - 4) + page.progressFS:SetJustifyH("LEFT") + page.progressFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + + page.reqLabel = content:CreateFontString(nil, "OVERLAY") + page.reqLabel:SetFont(GetFont(), 11, "OUTLINE") + page.reqLabel:SetWidth(CONTENT_W) + page.reqLabel:SetJustifyH("LEFT") + page.reqLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + page.reqLabel:SetText("需要的物品") + + for i = 1, MAX_ITEMS do progressItemSlots[i] = CreateItemSlot(content) end + + page.Update = function() + local y = 6 + page.titleFS:SetText(GetTitleText and GetTitleText() or "") + page.titleFS:ClearAllPoints() + page.titleFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + y = y + TextHeight(page.titleFS, 16) + 8 + + page.sepLine:ClearAllPoints() + page.sepLine:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + page.sepLine:Show(); y = y + 6 + + page.progressFS:SetText(GetProgressText and GetProgressText() or "") + page.progressFS:ClearAllPoints() + page.progressFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + y = y + TextHeight(page.progressFS, 14) + 12 + + local numReq = GetNumQuestItems and GetNumQuestItems() or 0 + for i = 1, MAX_ITEMS do progressItemSlots[i]:Clear() end + if numReq > 0 then + page.reqLabel:ClearAllPoints() + page.reqLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + page.reqLabel:Show(); y = y + 16 + for i = 1, math.min(numReq, MAX_ITEMS) do + progressItemSlots[i]:ClearAllPoints() + progressItemSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y) + progressItemSlots[i]:SetItemInfo("required", i) + y = y + ITEM_SIZE + ITEM_GAP + end + else + page.reqLabel:Hide() + end + content:SetHeight(y + 10) + end + return page +end +-------------------------------------------------------------------------------- +-- COMPLETE PAGE (reward selection & finish) +-------------------------------------------------------------------------------- +local completeRewardSlots = {} +local completeChoiceSlots = {} + +local function CreateCompletePage(parent) + local page = CreateFrame("Frame", nil, parent) + page:SetAllPoints(parent) + page:Hide() + + local scroll = CreateScrollArea(page, "SFramesQuestCompleteScroll") + scroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + scroll:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) + page.scroll = scroll + local content = scroll.content + + page.titleFS = content:CreateFontString(nil, "OVERLAY") + page.titleFS:SetFont(GetFont(), 14, "OUTLINE") + page.titleFS:SetWidth(CONTENT_W - 4) + page.titleFS:SetJustifyH("LEFT") + page.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + + page.sepLine = MakeSep(content, CONTENT_W) + + page.rewardTextFS = content:CreateFontString(nil, "OVERLAY") + page.rewardTextFS:SetFont(GetFont(), 12) + page.rewardTextFS:SetWidth(CONTENT_W - 4) + page.rewardTextFS:SetJustifyH("LEFT") + page.rewardTextFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + + page.rewardLabel = content:CreateFontString(nil, "OVERLAY") + page.rewardLabel:SetFont(GetFont(), 11, "OUTLINE") + page.rewardLabel:SetWidth(CONTENT_W) + page.rewardLabel:SetJustifyH("LEFT") + page.rewardLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + page.rewardLabel:SetText("你将获得") + + for i = 1, MAX_ITEMS do completeRewardSlots[i] = CreateItemSlot(content) end + + page.moneyLine = CreateMoneyLine(content) + + page.choiceLabel = content:CreateFontString(nil, "OVERLAY") + page.choiceLabel:SetFont(GetFont(), 11, "OUTLINE") + page.choiceLabel:SetWidth(CONTENT_W) + page.choiceLabel:SetJustifyH("LEFT") + page.choiceLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + page.choiceLabel:SetText("选择一项奖励") + + for i = 1, MAX_ITEMS do + local slot = CreateItemSlot(content) + local slotIdx = i + slot:SetScript("OnClick", function() + selectedReward = slotIdx + for si = 1, MAX_ITEMS do completeChoiceSlots[si]:SetSelected(si == slotIdx) end + UpdateBottomButtons() + end) + completeChoiceSlots[i] = slot + end + + page.Update = function() + selectedReward = 0 + local y = 6 + + page.titleFS:SetText(GetTitleText and GetTitleText() or "") + page.titleFS:ClearAllPoints() + page.titleFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + y = y + TextHeight(page.titleFS, 16) + 8 + + page.sepLine:ClearAllPoints() + page.sepLine:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + page.sepLine:Show(); y = y + 6 + + page.rewardTextFS:SetText(GetRewardText and GetRewardText() or "") + page.rewardTextFS:ClearAllPoints() + page.rewardTextFS:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + y = y + TextHeight(page.rewardTextFS, 14) + 12 + + local numRew = GetNumQuestRewards and GetNumQuestRewards() or 0 + local numCho = GetNumQuestChoices and GetNumQuestChoices() or 0 + local money = GetRewardMoney and GetRewardMoney() or 0 + + for i = 1, MAX_ITEMS do completeRewardSlots[i]:Clear(); completeChoiceSlots[i]:Clear() end + page.rewardLabel:Hide(); page.choiceLabel:Hide(); page.moneyLine:Hide() + + if numRew > 0 or money > 0 then + page.rewardLabel:ClearAllPoints() + page.rewardLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + page.rewardLabel:Show(); y = y + 16 + for i = 1, math.min(numRew, MAX_ITEMS) do + completeRewardSlots[i]:ClearAllPoints() + completeRewardSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y) + completeRewardSlots[i]:SetItemInfo("reward", i) + y = y + ITEM_SIZE + ITEM_GAP + end + if money > 0 then + page.moneyLine:ClearAllPoints() + page.moneyLine:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y) + page.moneyLine:SetMoney(money); y = y + 20 + end + y = y + 4 + end + + if numCho > 0 then + page.choiceLabel:ClearAllPoints() + page.choiceLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 2, -y) + page.choiceLabel:Show(); y = y + 16 + for i = 1, math.min(numCho, MAX_ITEMS) do + completeChoiceSlots[i]:ClearAllPoints() + completeChoiceSlots[i]:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y) + completeChoiceSlots[i]:SetItemInfo("choice", i) + completeChoiceSlots[i]:SetSelected(false) + y = y + ITEM_SIZE + ITEM_GAP + end + if numCho == 1 then + selectedReward = 1 + completeChoiceSlots[1]:SetSelected(true) + end + end + content:SetHeight(y + 10) + end + return page +end +-------------------------------------------------------------------------------- +-- INITIALIZE +-------------------------------------------------------------------------------- +function QUI:Initialize() + if MainFrame then return end + + MainFrame = CreateFrame("Frame", "SFramesQuestFrame", UIParent) + MainFrame:SetWidth(FRAME_W) + MainFrame:SetHeight(FRAME_H) + MainFrame:SetPoint("LEFT", UIParent, "LEFT", 64, 0) + MainFrame:SetFrameStrata("HIGH") + MainFrame:SetToplevel(true) + MainFrame:EnableMouse(true) + MainFrame:SetMovable(true) + MainFrame:RegisterForDrag("LeftButton") + MainFrame:SetScript("OnDragStart", function() this:StartMoving() end) + MainFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + SetRoundBackdrop(MainFrame) + CreateShadow(MainFrame) + + local header = CreateFrame("Frame", nil, MainFrame) + header:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 0, 0) + header:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 0, 0) + header:SetHeight(HEADER_H) + + local titleIco = SFrames:CreateIcon(header, "quest", 16) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0) + titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) + + local npcNameFS = header:CreateFontString(nil, "OVERLAY") + npcNameFS:SetFont(GetFont(), 14, "OUTLINE") + npcNameFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) + npcNameFS:SetPoint("RIGHT", header, "RIGHT", -30, 0) + npcNameFS:SetJustifyH("LEFT") + npcNameFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + MainFrame.npcNameFS = npcNameFS + + local closeBtn = CreateFrame("Button", nil, MainFrame, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 2, 2) + closeBtn:SetWidth(24); closeBtn:SetHeight(24) + + local headerSep = MainFrame:CreateTexture(nil, "ARTWORK") + headerSep:SetTexture("Interface\\Buttons\\WHITE8X8") + headerSep:SetHeight(1) + headerSep:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 6, -HEADER_H) + headerSep:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -6, -HEADER_H) + headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + local contentArea = CreateFrame("Frame", nil, MainFrame) + contentArea:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -(HEADER_H + 4)) + contentArea:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, BOTTOM_H + 4) + MainFrame.contentArea = contentArea + + local bottomSep = MainFrame:CreateTexture(nil, "ARTWORK") + bottomSep:SetTexture("Interface\\Buttons\\WHITE8X8") + bottomSep:SetHeight(1) + bottomSep:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", 6, BOTTOM_H) + bottomSep:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -6, BOTTOM_H) + bottomSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + MainFrame.btn1 = CreateActionBtn(MainFrame, "", 100) + MainFrame.btn1:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, 8) + MainFrame.btn1:Hide() + + MainFrame.btn2 = CreateActionBtn(MainFrame, "", 100) + MainFrame.btn2:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, 8) + MainFrame.btn2:Hide() + + pages.gossip = CreateGossipPage(contentArea) + pages.greeting = CreateGreetingPage(contentArea) + pages.detail = CreateDetailPage(contentArea) + pages.progress = CreateProgressPage(contentArea) + pages.complete = CreateCompletePage(contentArea) + + MainFrame:SetScript("OnHide", function() + if currentPage then + if CloseGossip then pcall(CloseGossip) end + if DeclineQuest then pcall(DeclineQuest) end + previousPage = nil + end + currentPage = nil + pendingClose = false + end) + + MainFrame:SetScript("OnUpdate", function() + if pendingClose then + closeTimer = closeTimer + (arg1 or 0) + if closeTimer > 0.15 then + pendingClose = false + closeTimer = 0 + previousPage = nil + if MainFrame:IsVisible() then + currentPage = nil + MainFrame:Hide() + end + end + end + end) + + MainFrame:Hide() + + MainFrame:RegisterEvent("GOSSIP_SHOW") + MainFrame:RegisterEvent("GOSSIP_CLOSED") + MainFrame:RegisterEvent("QUEST_GREETING") + MainFrame:RegisterEvent("QUEST_DETAIL") + MainFrame:RegisterEvent("QUEST_PROGRESS") + MainFrame:RegisterEvent("QUEST_COMPLETE") + MainFrame:RegisterEvent("QUEST_FINISHED") + MainFrame:SetScript("OnEvent", function() + if event == "GOSSIP_SHOW" then + pendingClose = false + local gText = GetGossipText() or "" + if GOSSIP_TRIGGER_TEXTS[strtrim and strtrim(gText) or gText] then return end + if TryAutoGossip() then return end + previousPage = "gossip" + MainFrame.npcNameFS:SetText(UnitName("npc") or "NPC") + ShowPage("gossip") + pages.gossip.Update() + UpdateBottomButtons() + MainFrame:Show() + + elseif event == "GOSSIP_CLOSED" then + if currentPage == "gossip" then + pendingClose = true + closeTimer = 0 + end + + elseif event == "QUEST_GREETING" then + pendingClose = false + previousPage = "greeting" + MainFrame.npcNameFS:SetText(UnitName("npc") or "NPC") + ShowPage("greeting") + pages.greeting.Update() + UpdateBottomButtons() + MainFrame:Show() + + elseif event == "QUEST_DETAIL" then + pendingClose = false + MainFrame.npcNameFS:SetText(UnitName("npc") or "NPC") + ShowPage("detail") + pages.detail.Update() + UpdateBottomButtons() + MainFrame:Show() + + elseif event == "QUEST_PROGRESS" then + pendingClose = false + MainFrame.npcNameFS:SetText(UnitName("npc") or "NPC") + ShowPage("progress") + pages.progress.Update() + UpdateBottomButtons() + MainFrame:Show() + + elseif event == "QUEST_COMPLETE" then + pendingClose = false + selectedReward = 0 + MainFrame.npcNameFS:SetText(UnitName("npc") or "NPC") + ShowPage("complete") + pages.complete.Update() + UpdateBottomButtons() + MainFrame:Show() + + elseif event == "QUEST_FINISHED" then + if previousPage then + pendingClose = true + closeTimer = 0 + else + pendingClose = false + currentPage = nil + MainFrame:Hide() + end + end + end) + + if GossipFrame then + GossipFrame:UnregisterEvent("GOSSIP_SHOW") + GossipFrame:UnregisterEvent("GOSSIP_CLOSED") + end + if QuestFrame then + QuestFrame:UnregisterEvent("QUEST_GREETING") + QuestFrame:UnregisterEvent("QUEST_DETAIL") + QuestFrame:UnregisterEvent("QUEST_PROGRESS") + QuestFrame:UnregisterEvent("QUEST_COMPLETE") + QuestFrame:UnregisterEvent("QUEST_FINISHED") + end + + tinsert(UISpecialFrames, "SFramesQuestFrame") +end + +-------------------------------------------------------------------------------- +-- Bootstrap +-------------------------------------------------------------------------------- +local bootstrap = CreateFrame("Frame") +bootstrap:RegisterEvent("PLAYER_LOGIN") +bootstrap:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + if SFramesDB.enableQuestUI == nil then + SFramesDB.enableQuestUI = true + end + if SFramesDB.enableQuestUI ~= false then + QUI:Initialize() + end + end +end) \ No newline at end of file diff --git a/Roll.lua b/Roll.lua new file mode 100644 index 0000000..1067517 --- /dev/null +++ b/Roll.lua @@ -0,0 +1,548 @@ +local AddOnName = "Nanami-UI" +SFrames = SFrames or {} + +-- ============================================================ +-- Roll Tracker Data +-- ============================================================ +local currentRolls = {} -- [itemName] = { [playerName] = { action, roll } } + +local _A = SFrames.ActiveTheme +local _bg = _A and _A.panelBg or { 0.08, 0.08, 0.10, 0.95 } +local _bd = _A and _A.panelBorder or { 0.3, 0.3, 0.35, 1 } + +local RollUI = CreateFrame("Frame") +RollUI:RegisterEvent("CHAT_MSG_LOOT") +RollUI:RegisterEvent("CHAT_MSG_SYSTEM") + +-- ============================================================ +-- String helpers (Lua 5.0 only - NO string.match!) +-- ============================================================ +local function StripLinks(msg) + return string.gsub(msg, "|c%x%x%x%x%x%x%x%x|H.-|h(.-)|h|r", "%1") +end + +local function GetBracketItem(text) + local _, _, name = string.find(text, "%[(.-)%]") + if name then return name end + text = string.gsub(text, "^%s+", "") + text = string.gsub(text, "%s+$", "") + text = string.gsub(text, "%.$", "") + return text +end + +local function TrimStr(s) + s = string.gsub(s, "^%s+", "") + s = string.gsub(s, "%s+$", "") + return s +end + +-- ============================================================ +-- Parse loot roll chat messages +-- ============================================================ +local function TrackRollEvent(msg) + if not msg or msg == "" then return nil end + + local clean = StripLinks(msg) + local player, rawItem, rollType, rollNum + + -- Patterns based on ACTUAL Turtle WoW Chinese message format: + -- "Buis选择了贪婪取向:[物品名]" + -- "Buis选择了需求取向:[物品名]" + + local _, _, p, i + + -- === PRIMARY: Turtle WoW Chinese format "选择了需求取向" / "选择了贪婪取向" === + _, _, p, i = string.find(clean, "^(.+)选择了需求取向:(.+)$") + if p and i then player = p; rawItem = i; rollType = "Need" end + + if not player then + _, _, p, i = string.find(clean, "^(.+)选择了需求取向: (.+)$") + if p and i then player = p; rawItem = i; rollType = "Need" end + end + + if not player then + _, _, p, i = string.find(clean, "^(.+)选择了贪婪取向:(.+)$") + if p and i then player = p; rawItem = i; rollType = "Greed" end + end + if not player then + _, _, p, i = string.find(clean, "^(.+)选择了贪婪取向: (.+)$") + if p and i then player = p; rawItem = i; rollType = "Greed" end + end + + -- Pass: "放弃了" format + if not player then + _, _, p, i = string.find(clean, "^(.+)放弃了:(.+)$") + if p and i then player = p; rawItem = i; rollType = "Pass" end + end + if not player then + _, _, p, i = string.find(clean, "^(.+)放弃了: (.+)$") + if p and i then player = p; rawItem = i; rollType = "Pass" end + end + if not player then + _, _, p, i = string.find(clean, "^(.+)放弃了(.+)$") + if p and i then player = p; rawItem = i; rollType = "Pass" end + end + + -- Roll (Chinese): "掷出 85 (1-100)" + if not player then + local r + _, _, p, r, i = string.find(clean, "^(.+)掷出%s*(%d+).-:(.+)$") + if p and r and i then player = p; rawItem = i; rollType = "Roll"; rollNum = r end + end + if not player then + local r + _, _, p, r, i = string.find(clean, "^(.+)掷出%s*(%d+).-: (.+)$") + if p and r and i then player = p; rawItem = i; rollType = "Roll"; rollNum = r end + end + + -- === FALLBACK: Other Chinese formats === + if not player then + _, _, p, i = string.find(clean, "^(.+)需求了:(.+)$") + if p and i then player = p; rawItem = i; rollType = "Need" end + end + if not player then + _, _, p, i = string.find(clean, "^(.+)贪婪了:(.+)$") + if p and i then player = p; rawItem = i; rollType = "Greed" end + end + + -- === ENGLISH patterns === + if not player then + _, _, p, i = string.find(clean, "^(.+) selected Need for: (.+)$") + if p and i then player = p; rawItem = i; rollType = "Need" end + end + if not player then + _, _, p, i = string.find(clean, "^(.+) selected Greed for: (.+)$") + if p and i then player = p; rawItem = i; rollType = "Greed" end + end + if not player then + _, _, p, i = string.find(clean, "^(.+) passed on: (.+)$") + if p and i then player = p; rawItem = i; rollType = "Pass" end + end + if not player then + local r + _, _, p, r, i = string.find(clean, "^(.+) rolls (%d+) .+for: (.+)$") + if p and r and i then player = p; rawItem = i; rollType = "Roll"; rollNum = r end + end + if not player then + _, _, p, i = string.find(clean, "^(.+) passes on (.+)$") + if p and i then player = p; rawItem = i; rollType = "Pass" end + end + + if player and rawItem then + player = TrimStr(player) + local itemName = GetBracketItem(rawItem) + + if itemName == "" or player == "" then return nil end + + if not currentRolls[itemName] then currentRolls[itemName] = {} end + if not currentRolls[itemName][player] then currentRolls[itemName][player] = {} end + + if rollType == "Roll" then + currentRolls[itemName][player].roll = rollNum + else + currentRolls[itemName][player].action = rollType + end + return itemName + end + return nil +end + +-- ============================================================ +-- Update the tracker text on visible roll frames +-- ============================================================ +local function UpdateRollTrackers() + for idx = 1, 4 do + local frame = _G["GroupLootFrame"..idx] + if frame and frame:IsVisible() and frame.rollID then + local _, itemName = GetLootRollItemInfo(frame.rollID) + + if itemName and frame.sfNeedFS then + local data = currentRolls[itemName] + local needText, greedText, passText = "", "", "" + if data then + local needs, greeds, passes = {}, {}, {} + for pl, info in pairs(data) do + local hex = SFrames and SFrames:GetClassHexForName(pl) + local coloredPl = hex and ("|cff" .. hex .. pl .. "|r") or pl + local rollStr = "" + if info.roll then rollStr = "|cffaaaaaa(" .. info.roll .. ")|r" end + if info.action == "Need" then + table.insert(needs, coloredPl .. rollStr) + elseif info.action == "Greed" then + table.insert(greeds, coloredPl .. rollStr) + elseif info.action == "Pass" then + table.insert(passes, coloredPl) + end + end + if table.getn(needs) > 0 then + needText = "|cffff5555需求|r " .. table.concat(needs, " ") + end + if table.getn(greeds) > 0 then + greedText = "|cff55ff55贪婪|r " .. table.concat(greeds, " ") + end + if table.getn(passes) > 0 then + passText = "|cff888888放弃|r " .. table.concat(passes, " ") + end + end + frame.sfNeedFS:SetText(needText) + frame.sfGreedFS:SetText(greedText) + frame.sfPassFS:SetText(passText) + end + end + end +end + +-- ============================================================ +-- Event Handler +-- ============================================================ +RollUI:SetScript("OnEvent", function() + if event == "CHAT_MSG_LOOT" or event == "CHAT_MSG_SYSTEM" then + local matched = TrackRollEvent(arg1) + if matched then + UpdateRollTrackers() + end + end +end) + +-- ============================================================ +-- Kill ALL Blizzard textures on the main frame only +-- ============================================================ +local function NukeBlizzTextures(frame) + local regions = {frame:GetRegions()} + for _, r in ipairs(regions) do + if r:IsObjectType("Texture") and not r.sfKeep then + r:SetTexture(nil) + r:SetAlpha(0) + r:Hide() + -- Don't override Show! SetBackdrop needs it. + -- Instead mark as nuked and move off-screen + if not r.sfNuked then + r.sfNuked = true + r:ClearAllPoints() + r:SetPoint("CENTER", UIParent, "CENTER", 9999, 9999) + end + end + end +end + +-- ============================================================ +-- Style the GroupLootFrame +-- ============================================================ +local function StyleGroupLootFrame(frame) + if frame.sfSkinned then return end + frame.sfSkinned = true + + local fname = frame:GetName() + + -- 1) Kill all Blizz textures on main frame + NukeBlizzTextures(frame) + + -- 2) Alt-Drag + frame:SetMovable(true) + frame:EnableMouse(true) + frame:RegisterForDrag("LeftButton") + frame:SetScript("OnDragStart", function() + if IsAltKeyDown() then + this:StartMoving() + end + end) + frame:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + end) + + -- 3) Rounded backdrop + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + frame:SetBackdropColor(_bg[1], _bg[2], _bg[3], _bg[4]) + frame:SetBackdropBorderColor(_bd[1], _bd[2], _bd[3], _bd[4]) + + -- Tag all backdrop regions so NukeBlizzTextures won't kill them + local bdRegions = {frame:GetRegions()} + for _, r in ipairs(bdRegions) do + r.sfKeep = true + end + + -- 5) Frame size (taller for tracker area) + frame:SetWidth(300) + frame:SetHeight(100) + + -- 6) Icon + local iconFrame = _G[fname.."IconFrame"] + if iconFrame then + iconFrame:ClearAllPoints() + iconFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -10) + iconFrame:SetWidth(36) + iconFrame:SetHeight(36) + + -- Kill IconFrame's own textures + local iRegions = {iconFrame:GetRegions()} + for _, r in ipairs(iRegions) do + if r:IsObjectType("Texture") then + local tName = r:GetName() or "" + if string.find(tName, "Icon") then + r:SetTexCoord(0.08, 0.92, 0.08, 0.92) + r:ClearAllPoints() + r:SetAllPoints(iconFrame) + r.sfKeep = true + else + r:SetTexture(nil) + r:SetAlpha(0) + r:Hide() + r.sfNuked = true + r:ClearAllPoints() + r:SetPoint("CENTER", UIParent, "CENTER", 9999, 9999) + end + end + end + + -- Clean icon border + local iconBorder = frame:CreateTexture(nil, "ARTWORK") + iconBorder:SetTexture("Interface\\Buttons\\WHITE8X8") + iconBorder:SetVertexColor(0.4, 0.4, 0.45, 1) + iconBorder:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", -2, 2) + iconBorder:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", 2, -2) + iconBorder.sfKeep = true + + local qualGlow = frame:CreateTexture(nil, "OVERLAY") + qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + qualGlow:SetBlendMode("ADD") + qualGlow:SetAlpha(0.8) + qualGlow:SetWidth(68) + qualGlow:SetHeight(68) + qualGlow:SetPoint("CENTER", iconFrame, "CENTER", 0, 0) + qualGlow:Hide() + qualGlow.sfKeep = true + frame.sfQualGlow = qualGlow + end + + -- 7) Item Name (shorter width to make room for buttons) + local itemNameFS = _G[fname.."Name"] + if itemNameFS then + itemNameFS:ClearAllPoints() + itemNameFS:SetPoint("LEFT", iconFrame, "RIGHT", 10, 0) + itemNameFS:SetWidth(145) + itemNameFS:SetHeight(36) + itemNameFS:SetJustifyH("LEFT") + itemNameFS:SetJustifyV("MIDDLE") + end + + -- 8) Buttons: right side, vertically centered with icon + local need = _G[fname.."NeedButton"] + local greed = _G[fname.."GreedButton"] + local pass = _G[fname.."PassButton"] + + -- Create our own close/pass button since Blizz keeps hiding PassButton + local closeBtn = CreateFrame("Button", nil, frame) + closeBtn:SetWidth(20) + closeBtn:SetHeight(20) + closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6) + closeBtn:SetFrameLevel(frame:GetFrameLevel() + 10) + + -- Rounded backdrop matching UI + closeBtn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 8, + insets = { left = 2, right = 2, top = 2, bottom = 2 } + }) + local _cbg = _A and _A.closeBtnBg or { 0.5, 0.1, 0.1, 0.85 } + local _cbd = _A and _A.closeBtnBorder or { 0.6, 0.2, 0.2, 0.8 } + local _cbgH = _A and _A.closeBtnHoverBg or { 0.7, 0.15, 0.15, 1 } + local _cbdH = _A and _A.closeBtnHoverBorder or { 0.9, 0.3, 0.3, 1 } + closeBtn:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4]) + closeBtn:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4]) + + -- "X" text + local closeText = closeBtn:CreateFontString(nil, "OVERLAY") + closeText:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 11, "OUTLINE") + closeText:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) + closeText:SetText("X") + closeText:SetTextColor(0.9, 0.8, 0.8) + + closeBtn:SetScript("OnClick", function() + local parent = this:GetParent() + if parent and parent.rollID then + ConfirmLootRoll(parent.rollID, 0) + end + end) + closeBtn:SetScript("OnEnter", function() + this:SetBackdropColor(_cbgH[1], _cbgH[2], _cbgH[3], _cbgH[4]) + this:SetBackdropBorderColor(_cbdH[1], _cbdH[2], _cbdH[3], _cbdH[4]) + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText("放弃 / Pass") + GameTooltip:Show() + end) + closeBtn:SetScript("OnLeave", function() + this:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4]) + this:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4]) + GameTooltip:Hide() + end) + + -- Position need/greed to the left of close button + if need and greed then + greed:ClearAllPoints() + greed:SetPoint("RIGHT", closeBtn, "LEFT", -6, 0) + greed:SetWidth(22); greed:SetHeight(22) + greed:Show() + + need:ClearAllPoints() + need:SetPoint("RIGHT", greed, "LEFT", -4, 0) + need:SetWidth(22); need:SetHeight(22) + need:Show() + end + + -- Hide Blizz pass button + if pass then + pass:ClearAllPoints() + pass:SetPoint("CENTER", UIParent, "CENTER", 9999, 9999) + pass:SetAlpha(0) + end + + -- 9) HIDE Blizz timer completely + local blizzTimer = _G[fname.."Timer"] + if blizzTimer then + blizzTimer:SetAlpha(0) + blizzTimer:ClearAllPoints() + blizzTimer:SetPoint("BOTTOMLEFT", frame, "TOPLEFT", 0, 500) + end + + -- 10) Our own timer bar + local myTimer = CreateFrame("StatusBar", nil, frame) + myTimer:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 12, 8) + myTimer:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 8) + myTimer:SetHeight(6) + myTimer:SetMinMaxValues(0, 1) + myTimer:SetValue(1) + myTimer:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar") + myTimer:SetStatusBarColor(0.9, 0.7, 0.15) + + local myTimerBg = myTimer:CreateTexture(nil, "BACKGROUND") + myTimerBg:SetTexture("Interface\\Buttons\\WHITE8X8") + myTimerBg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + myTimerBg:SetAllPoints(myTimer) + + frame.sfTimer = myTimer + frame.sfTimerMax = nil -- will be set on first OnUpdate + + -- 11) OnUpdate: record max time on first tick, then animate + frame:SetScript("OnUpdate", function() + if this.rollID and this.sfTimer then + local timeLeft = GetLootRollTimeLeft(this.rollID) + if timeLeft and timeLeft > 0 then + if not this.sfTimerMax or this.sfTimerMax < timeLeft then + this.sfTimerMax = timeLeft + end + this.sfTimer:SetMinMaxValues(0, this.sfTimerMax) + this.sfTimer:SetValue(timeLeft) + else + this.sfTimer:SetValue(0) + end + end + end) + + -- 12) Clean button textures so they don't extend beyond bounds + if need then + local nRegions = {need:GetRegions()} + for _, r in ipairs(nRegions) do + if r:IsObjectType("Texture") then + r:ClearAllPoints() + r:SetAllPoints(need) + end + end + end + if greed then + local gRegions = {greed:GetRegions()} + for _, r in ipairs(gRegions) do + if r:IsObjectType("Texture") then + r:ClearAllPoints() + r:SetAllPoints(greed) + end + end + end + + -- 13) Three FontStrings for Need / Greed / Pass (vertical stack, full width) + local textWidth = 276 + + local needFS = frame:CreateFontString(nil, "OVERLAY") + needFS:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 10, "OUTLINE") + needFS:SetPoint("TOPLEFT", iconFrame, "BOTTOMLEFT", 0, -4) + needFS:SetWidth(textWidth) + needFS:SetJustifyH("LEFT") + needFS:SetJustifyV("TOP") + needFS:SetTextColor(1, 1, 1) + frame.sfNeedFS = needFS + + local greedFS = frame:CreateFontString(nil, "OVERLAY") + greedFS:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 10, "OUTLINE") + greedFS:SetPoint("TOPLEFT", needFS, "BOTTOMLEFT", 0, -1) + greedFS:SetWidth(textWidth) + greedFS:SetJustifyH("LEFT") + greedFS:SetJustifyV("TOP") + greedFS:SetTextColor(1, 1, 1) + frame.sfGreedFS = greedFS + + local passFS = frame:CreateFontString(nil, "OVERLAY") + passFS:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 10, "OUTLINE") + passFS:SetPoint("TOPLEFT", greedFS, "BOTTOMLEFT", 0, -1) + passFS:SetWidth(textWidth) + passFS:SetJustifyH("LEFT") + passFS:SetJustifyV("TOP") + passFS:SetTextColor(1, 1, 1) + frame.sfPassFS = passFS +end + +-- ============================================================ +-- Hooks +-- ============================================================ +local function RestoreBackdrop(frame) + if not frame.sfSkinned then return end + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + frame:SetBackdropColor(_bg[1], _bg[2], _bg[3], _bg[4]) + frame:SetBackdropBorderColor(_bd[1], _bd[2], _bd[3], _bd[4]) +end + +local function UpdateRollQualityGlow(frame) + if frame.sfQualGlow and frame.rollID then + local _, _, _, quality = GetLootRollItemInfo(frame.rollID) + if quality and quality > 1 then + local r, g, b = GetItemQualityColor(quality) + frame.sfQualGlow:SetVertexColor(r, g, b) + frame.sfQualGlow:Show() + else + frame.sfQualGlow:Hide() + end + end +end + +local Hook_GroupLootFrame_OnShow = GroupLootFrame_OnShow +function GroupLootFrame_OnShow() + if Hook_GroupLootFrame_OnShow then Hook_GroupLootFrame_OnShow() end + StyleGroupLootFrame(this) + NukeBlizzTextures(this) + RestoreBackdrop(this) + UpdateRollQualityGlow(this) + UpdateRollTrackers() +end + +local Hook_GroupLootFrame_Update = GroupLootFrame_Update +function GroupLootFrame_Update() + if Hook_GroupLootFrame_Update then Hook_GroupLootFrame_Update() end + for idx = 1, 4 do + local f = _G["GroupLootFrame"..idx] + if f and f:IsVisible() and f.sfSkinned then + NukeBlizzTextures(f) + RestoreBackdrop(f) + UpdateRollQualityGlow(f) + end + end + UpdateRollTrackers() +end diff --git a/SellPriceDB.lua b/SellPriceDB.lua new file mode 100644 index 0000000..6b5fefc --- /dev/null +++ b/SellPriceDB.lua @@ -0,0 +1,3983 @@ +-- Nanami-UI Embedded Sell Price Database +-- Merged: ShaguTweaks Turtle WoW (priority) + Informant + SellValue wowhead data +-- [itemID] = vendorSellPrice (in copper) +-- Total entries: 23853 + +NanamiSellPriceDB = { + [1]=0, [7]=0, [8]=1584, [9]=0, [10]=46, [11]=0, + [13]=5, [14]=0, [16]=22500, [17]=32, [18]=32, [19]=32, + [20]=2, [21]=2, [22]=0, [24]=2300, [25]=7, [26]=9, + [27]=7, [28]=7, [29]=0, [31]=1800, [33]=0, [35]=9, + [36]=7, [37]=7, [38]=1, [39]=1, [40]=1, [42]=1, + [43]=1, [44]=1, [45]=1, [46]=1, [47]=1, [48]=1, + [49]=1, [50]=1, [51]=1, [52]=1, [53]=1, [54]=1, + [55]=1, [56]=1, [57]=1, [58]=1, [59]=1, [60]=12, + [61]=12, [62]=0, [64]=11, [65]=1000, [66]=150, [67]=150, + [68]=5000, [69]=1625, [70]=2800, [71]=1800, [74]=1800, [75]=1900, + [76]=1800, [77]=1800, [79]=10, [80]=7, [81]=0, [82]=150, + [83]=2300, [84]=0, [85]=12, [86]=0, [87]=150, [88]=3600, + [89]=1, [90]=1, [91]=1, [92]=1, [93]=1, [94]=0, + [95]=32, [96]=32, [97]=32, [98]=32, [99]=32, [100]=32, + [101]=32, [102]=32, [103]=150, [104]=12500, [112]=0, [113]=4800, + [117]=1, [118]=5, [119]=0, [120]=1, [121]=1, [122]=1, + [123]=0, [124]=10500, [127]=1, [128]=0, [129]=1, [130]=0, + [131]=150, [132]=25000, [137]=1, [138]=0, [139]=1, [140]=1, + [141]=1, [142]=0, [143]=1, [146]=1, [147]=1, [148]=1, + [151]=10, [152]=1, [153]=1, [154]=1, [156]=3682, [159]=1, + [193]=9, [194]=9, [195]=7, [200]=454, [201]=455, [202]=342, + [203]=229, [209]=12, [210]=9, [212]=32, [236]=559, [237]=561, + [238]=422, [239]=282, [285]=711, [286]=645, [287]=488, [414]=6, + [422]=25, [537]=87, [555]=8, [556]=106, [647]=70024, [710]=72, + [711]=5, [714]=6, [718]=322, [719]=4, [720]=1070, [723]=15, + [724]=25, [727]=244, [728]=50, [729]=17, [730]=16, [731]=27, + [732]=6, [733]=100, [744]=10000, [753]=3842, [754]=23556, [755]=1, + [756]=4963, [763]=58, [765]=10, [766]=57, [767]=100, [768]=113, + [769]=3, [770]=316, [771]=38, [774]=15, [776]=5765, [777]=21, + [778]=55, [779]=18, [781]=110, [783]=50, [785]=20, [787]=1, + [789]=1969, [790]=2020, [791]=7069, [792]=41, [793]=27, [794]=55, + [795]=56, [796]=52, [797]=35, [798]=70, [799]=71, [804]=2500, + [805]=250, [809]=39125, [810]=45267, [811]=54123, [812]=57018, [813]=58, + [814]=25, [816]=305, [818]=100, [820]=947, [821]=249, [826]=732, + [827]=971, [828]=250, [832]=203, [833]=28000, [835]=23, [837]=224, + [838]=225, [839]=113, [840]=170, [843]=215, [844]=144, [845]=289, + [846]=290, [847]=349, [848]=351, [849]=265, [850]=176, [851]=404, + [852]=347, [853]=481, [854]=604, [856]=875, [857]=2500, [858]=25, + [859]=87, [860]=89, [862]=38222, [863]=8964, [864]=9716, [865]=5096, + [866]=16640, [867]=5344, [868]=21532, [869]=18529, [870]=21528, [871]=29627, + [872]=2362, [873]=21770, [875]=1, [876]=1, [878]=56, [880]=2692, + [885]=2300, [886]=2949, [887]=82, [888]=980, [890]=3517, [892]=369, + [893]=137, [897]=1022, [899]=1248, [911]=3324, [913]=5037, [914]=2669, + [918]=1250, [920]=2821, [922]=2407, [923]=1748, [924]=2194, [925]=1559, + [926]=1956, [927]=1390, [928]=1972, [929]=75, [932]=637, [933]=2500, + [934]=10682, [935]=1742, [936]=11620, [937]=14577, [940]=12566, [942]=4500, + [943]=42863, [944]=83000, [945]=3, [948]=3, [954]=50, [955]=37, + [961]=2, [964]=12, [966]=87, [967]=1050, [968]=20, [973]=250, + [974]=75, [975]=187, [976]=625, [980]=750, [983]=21, [985]=550, + [986]=50, [989]=75, [992]=125, [994]=2425, [997]=3, [1002]=1675, + [1004]=2000, [1008]=144, [1009]=490, [1010]=99, [1011]=80, [1015]=24, + [1017]=100, [1029]=20, [1030]=187, [1031]=20, [1032]=87, [1033]=625, + [1034]=1000, [1035]=187, [1036]=450, [1037]=75, [1038]=87, [1048]=325, + [1049]=150, [1052]=325, [1053]=225, [1057]=450, [1058]=550, [1061]=1500, + [1063]=1500, [1072]=62, [1074]=491, [1076]=650, [1077]=25, [1080]=78, + [1081]=50, [1082]=150, [1084]=50, [1085]=1050, [1086]=150, [1087]=10, + [1088]=225, [1089]=325, [1090]=75, [1091]=20, [1092]=10, [1093]=1000, + [1095]=550, [1096]=75, [1100]=225, [1101]=2750, [1102]=75, [1105]=4500, + [1108]=725, [1109]=2000, [1111]=50, [1112]=150, [1116]=1250, [1117]=2, + [1119]=50, [1121]=1075, [1127]=25, [1131]=1136, [1132]=0, [1134]=0, + [1136]=550, [1138]=10, [1139]=2425, [1141]=20, [1144]=3250, [1149]=750, + [1150]=20, [1151]=225, [1154]=102, [1155]=5954, [1156]=812, [1158]=146, + [1159]=32, [1161]=25, [1166]=16, [1167]=96, [1168]=42296, [1169]=18815, + [1171]=55, [1172]=968, [1173]=52, [1175]=28, [1176]=32, [1177]=3, + [1178]=7, [1179]=6, [1180]=37, [1181]=25, [1182]=44, [1183]=29, + [1187]=1081, [1189]=625, [1190]=630, [1191]=82, [1193]=355, [1194]=20, + [1195]=47, [1196]=442, [1197]=533, [1198]=535, [1200]=16, [1201]=94, + [1202]=367, [1203]=23505, [1204]=12577, [1205]=25, [1206]=400, [1207]=9892, + [1210]=250, [1211]=347, [1212]=21, [1213]=87, [1214]=930, [1215]=469, + [1216]=1197, [1218]=2064, [1219]=823, [1220]=1807, [1222]=14, [1224]=1325, + [1228]=50, [1229]=550, [1231]=25, [1232]=75, [1238]=50, [1239]=1675, + [1243]=150, [1244]=1050, [1245]=75, [1246]=325, [1250]=1675, [1251]=10, + [1262]=111, [1263]=79064, [1264]=3671, [1265]=11774, [1270]=213, [1273]=1656, + [1274]=8, [1275]=1668, [1276]=2220, [1280]=3696, [1282]=2825, [1287]=703, + [1288]=185, [1292]=3300, [1296]=1681, [1297]=2038, [1299]=390, [1300]=1854, + [1302]=262, [1303]=418, [1304]=279, [1306]=352, [1310]=715, [1314]=362, + [1315]=13000, [1317]=3500, [1318]=3066, [1319]=462, [1322]=68, [1326]=10, + [1328]=550, [1332]=225, [1334]=75, [1339]=50, [1341]=10, [1351]=768, + [1352]=200, [1355]=251, [1359]=19, [1360]=42, [1364]=8, [1366]=2, + [1367]=2, [1368]=2, [1369]=4, [1370]=2, [1372]=2, [1374]=3, + [1376]=4, [1377]=1, [1378]=1, [1380]=4, [1382]=24, [1383]=25, + [1384]=11, [1386]=32, [1387]=1578, [1388]=14, [1389]=58, [1391]=1392, + [1394]=740, [1395]=1, [1396]=1, [1399]=12, [1400]=337, [1401]=14, + [1402]=162, [1404]=10306, [1405]=1184, [1406]=2079, [1411]=68, [1412]=49, + [1413]=55, [1414]=97, [1415]=72, [1416]=72, [1417]=65, [1418]=9, + [1419]=19, [1420]=18, [1421]=28, [1422]=6, [1423]=18, [1425]=37, + [1427]=29, [1429]=7, [1430]=7, [1431]=20, [1433]=14, [1434]=43, + [1436]=458, [1438]=70, [1440]=1230, [1443]=21125, [1445]=583, [1446]=568, + [1447]=22775, [1448]=405, [1449]=1875, [1450]=10, [1454]=3937, [1455]=2931, + [1457]=1850, [1458]=2623, [1459]=2380, [1460]=1768, [1461]=3387, [1462]=1306, + [1464]=71, [1465]=9824, [1468]=28, [1469]=1180, [1470]=875, [1473]=1497, + [1475]=82, [1476]=6, [1477]=87, [1478]=62, [1479]=47, [1480]=954, + [1481]=3337, [1482]=2964, [1483]=2025, [1484]=2922, [1485]=1410, [1486]=1202, + [1488]=3379, [1489]=1553, [1490]=8910, [1491]=2207, [1493]=3922, [1495]=58, + [1497]=71, [1498]=57, [1499]=51, [1501]=80, [1502]=60, [1503]=109, + [1504]=32, [1505]=48, [1506]=51, [1507]=123, [1509]=59, [1510]=150, + [1511]=193, [1512]=194, [1513]=293, [1514]=294, [1515]=196, [1516]=237, + [1520]=71, [1521]=19205, [1522]=10224, [1523]=10261, [1529]=700, [1534]=1050, + [1536]=2425, [1537]=62, [1539]=1572, [1547]=2795, [1554]=225, [1557]=918, + [1559]=325, [1560]=513, [1561]=268, [1566]=1132, [1567]=2425, [1568]=2000, + [1571]=1675, [1574]=725, [1588]=1050, [1589]=1750, [1591]=1325, [1597]=750, + [1602]=11685, [1603]=5000, [1604]=19463, [1607]=43420, [1608]=18940, [1613]=19139, + [1619]=2000, [1623]=5000, [1624]=6172, [1625]=14754, [1630]=66, [1639]=28460, + [1640]=15637, [1641]=550, [1645]=100, [1648]=3250, [1651]=3750, [1652]=5000, + [1657]=1675, [1658]=1675, [1659]=2136, [1664]=14695, [1676]=750, [1677]=10093, + [1678]=4581, [1679]=9635, [1680]=18234, [1681]=450, [1685]=6250, [1686]=733, + [1687]=243, [1688]=806, [1696]=606, [1697]=445, [1701]=376, [1702]=320, + [1703]=81, [1705]=600, [1706]=86, [1707]=62, [1708]=50, [1710]=125, + [1711]=75, [1712]=62, [1713]=5350, [1714]=2535, [1715]=10301, [1716]=5067, + [1717]=3108, [1718]=8040, [1720]=26209, [1721]=35815, [1722]=19407, [1725]=5000, + [1726]=12183, [1727]=4562, [1728]=87008, [1729]=500, [1730]=48, [1731]=92, + [1732]=73, [1733]=88, [1734]=39, [1735]=89, [1737]=139, [1738]=147, + [1739]=255, [1740]=97, [1741]=112, [1742]=129, [1743]=299, [1744]=260, + [1745]=198, [1746]=332, [1747]=302, [1748]=232, [1749]=396, [1750]=298, + [1751]=676, [1752]=286, [1753]=439, [1754]=359, [1755]=597, [1756]=438, + [1757]=728, [1758]=534, [1759]=732, [1760]=609, [1761]=892, [1764]=113, + [1766]=131, [1767]=101, [1768]=217, [1769]=163, [1770]=143, [1772]=247, + [1774]=281, [1775]=212, [1776]=257, [1777]=222, [1778]=337, [1780]=489, + [1782]=541, [1783]=247, [1784]=546, [1785]=464, [1786]=683, [1787]=102, + [1788]=176, [1789]=136, [1790]=93, [1791]=90, [1792]=208, [1793]=207, + [1794]=277, [1795]=235, [1796]=400, [1797]=161, [1798]=279, [1799]=211, + [1800]=479, [1801]=418, [1802]=337, [1803]=465, [1804]=478, [1805]=351, + [1806]=466, [1807]=387, [1808]=856, [1809]=440, [1810]=648, [1811]=451, + [1812]=452, [1813]=522, [1814]=603, [1815]=366, [1816]=486, [1817]=501, + [1818]=1221, [1819]=768, [1820]=963, [1821]=988, [1822]=1096, [1823]=779, + [1824]=1104, [1825]=1548, [1826]=1765, [1827]=1282, [1828]=1609, [1829]=1563, + [1830]=1783, [1831]=1790, [1832]=362, [1835]=6, [1836]=6, [1839]=36, + [1840]=37, [1843]=145, [1844]=145, [1845]=175, [1846]=176, [1849]=277, + [1850]=278, [1851]=1785, [1852]=336, [1853]=338, [1877]=325, [1882]=325, + [1886]=75, [1893]=1775, [1895]=3, [1896]=3, [1897]=3, [1899]=3, + [1900]=3, [1901]=3, [1902]=3, [1903]=3, [1904]=3, [1905]=3, + [1906]=3, [1907]=3, [1908]=4, [1909]=3, [1910]=3, [1911]=3, + [1913]=148, [1917]=251, [1925]=787, [1926]=687, [1927]=690, [1928]=995, + [1929]=434, [1930]=408, [1933]=905, [1934]=731, [1935]=2974, [1936]=1113, + [1937]=1700, [1938]=1962, [1939]=168, [1941]=203, [1942]=316, [1943]=713, + [1944]=259, [1945]=267, [1951]=1258, [1955]=1573, [1957]=1, [1958]=975, + [1959]=1223, [1961]=1, [1965]=36, [1970]=120, [1973]=4618, [1974]=464, + [1975]=5744, [1976]=6340, [1978]=980, [1979]=23273, [1980]=6200, [1981]=14113, + [1982]=29513, [1983]=4, [1984]=2, [1985]=2, [1986]=14861, [1988]=2852, + [1990]=10077, [1991]=8357, [1992]=5468, [1993]=2100, [1994]=16696, [1996]=1720, + [1997]=2538, [1998]=7239, [2000]=8827, [2011]=3840, [2013]=3644, [2014]=4869, + [2015]=4443, [2016]=1769, [2017]=895, [2018]=3269, [2020]=1049, [2021]=1025, + [2023]=2, [2024]=1215, [2025]=1060, [2026]=1257, [2027]=763, [2028]=1013, + [2029]=883, [2030]=1108, [2032]=1665, [2033]=967, [2034]=1035, [2035]=2300, + [2036]=258, [2037]=469, [2039]=750, [2040]=15296, [2041]=1412, [2042]=3639, + [2043]=10209, [2044]=8826, [2046]=2461, [2047]=25, [2048]=25, [2051]=1, + [2052]=1, [2053]=1, [2054]=16, [2055]=16, [2056]=92, [2057]=16, + [2058]=4197, [2059]=863, [2064]=191, [2065]=113, [2066]=81, [2067]=186, + [2069]=121, [2070]=1, [2072]=4414, [2073]=742, [2074]=1054, [2075]=386, + [2077]=5059, [2078]=1070, [2079]=933, [2080]=6590, [2082]=250, [2084]=5709, + [2085]=15, [2087]=252, [2088]=729, [2089]=1113, [2090]=1580, [2091]=213, + [2092]=7, [2098]=3020, [2099]=45040, [2100]=24539, [2101]=1, [2102]=1, + [2105]=1, [2108]=8, [2109]=14, [2110]=6, [2112]=54, [2114]=31, + [2117]=7, [2119]=4, [2120]=10, [2121]=10, [2122]=6, [2123]=9, + [2124]=6, [2125]=6, [2126]=11, [2127]=12, [2128]=20, [2129]=15, + [2130]=10, [2131]=10, [2132]=20, [2133]=15, [2134]=16, [2137]=24, + [2138]=38, [2139]=11, [2140]=323, [2141]=1044, [2142]=524, [2143]=788, + [2144]=527, [2145]=529, [2146]=961, [2147]=3, [2148]=581, [2149]=879, + [2150]=586, [2151]=588, [2152]=1181, [2153]=1185, [2156]=615, [2158]=413, + [2159]=829, [2160]=832, [2163]=46710, [2164]=27031, [2165]=45, [2166]=810, + [2167]=339, [2168]=469, [2169]=943, [2172]=7, [2173]=6, [2175]=2683, + [2176]=4, [2177]=4, [2178]=3, [2179]=3, [2180]=3, [2181]=3, + [2182]=3, [2183]=3, [2184]=3, [2186]=6, [2194]=3276, [2195]=25, + [2196]=2, [2197]=540, [2198]=2, [2199]=2, [2200]=2, [2201]=2, + [2202]=2, [2203]=1492, [2204]=1133, [2205]=3961, [2207]=478, [2208]=730, + [2209]=1423, [2210]=3, [2211]=7, [2212]=16, [2213]=24, [2214]=182, + [2215]=81, [2216]=210, [2217]=243, [2218]=501, [2219]=457, [2220]=519, + [2221]=910, [2222]=1005, [2224]=24, [2225]=183, [2226]=4117, [2227]=4133, + [2230]=714, [2231]=4386, [2232]=812, [2233]=1690, [2234]=1806, [2235]=1281, + [2236]=3386, [2237]=75, [2238]=60, [2240]=151, [2241]=615, [2243]=70554, + [2244]=51857, [2245]=27636, [2246]=30000, [2249]=91, [2251]=12, [2254]=506, + [2256]=2996, [2257]=188, [2258]=83, [2259]=75, [2260]=106, [2262]=8746, + [2263]=2619, [2264]=1957, [2265]=477, [2266]=479, [2267]=711, [2268]=146, + [2271]=3255, [2273]=436, [2274]=289, [2276]=4093, [2277]=3620, [2278]=2326, + [2280]=4023, [2281]=300, [2282]=139, [2283]=140, [2284]=211, [2287]=6, + [2289]=87, [2290]=75, [2291]=44580, [2292]=1334, [2295]=70, [2296]=50, + [2299]=8756, [2300]=192, [2302]=29, [2303]=71, [2304]=15, [2307]=385, + [2308]=267, [2309]=268, [2310]=112, [2311]=150, [2312]=181, [2313]=200, + [2314]=1356, [2315]=547, [2316]=561, [2317]=689, [2318]=15, [2319]=50, + [2320]=2, [2321]=25, [2324]=6, [2325]=250, [2326]=29, [2327]=36, + [2361]=9, [2362]=1, [2364]=59, [2366]=59, [2367]=44, [2369]=30, + [2370]=75, [2371]=37, [2372]=68, [2373]=51, [2374]=34, [2375]=34, + [2376]=89, [2377]=89, [2379]=15, [2380]=7, [2381]=15, [2383]=11, + [2384]=7, [2385]=7, [2386]=15, [2387]=7, [2388]=15, [2389]=11, + [2390]=7, [2391]=7, [2392]=82, [2393]=41, [2394]=83, [2395]=64, + [2396]=43, [2397]=43, [2398]=86, [2399]=43, [2400]=87, [2401]=66, + [2402]=43, [2403]=44, [2406]=25, [2407]=162, [2408]=125, [2409]=350, + [2410]=1, [2411]=0, [2413]=0, [2414]=0, [2417]=3134, [2418]=3146, + [2419]=1578, [2420]=2387, [2421]=1590, [2422]=1596, [2423]=8554, [2424]=4292, + [2425]=8615, [2426]=6513, [2427]=4029, [2428]=4044, [2429]=2027, [2431]=2043, + [2432]=1538, [2434]=1033, [2435]=5536, [2437]=5578, [2438]=4199, [2440]=2819, + [2441]=729, [2442]=1312, [2443]=3417, [2444]=9393, [2445]=686, [2446]=1236, + [2447]=10, [2448]=3231, [2449]=20, [2450]=25, [2451]=8725, [2452]=15, + [2453]=25, [2454]=20, [2455]=10, [2456]=15, [2457]=15, [2458]=15, + [2459]=25, [2460]=25, [2463]=2739, [2464]=1374, [2465]=2495, [2467]=1886, + [2468]=1262, [2469]=1266, [2470]=6790, [2471]=3408, [2472]=6842, [2473]=5150, + [2474]=3446, [2475]=3459, [2479]=21, [2480]=14, [2481]=17, [2482]=11, + [2483]=14, [2484]=17, [2485]=10, [2486]=13, [2487]=20, [2488]=107, + [2489]=68, [2490]=108, [2491]=96, [2492]=56, [2493]=140, [2494]=80, + [2495]=100, [2496]=80, [2497]=142, [2498]=81, [2499]=143, [2500]=58, + [2501]=144, [2502]=59, [2503]=103, [2504]=5, [2505]=12, [2506]=57, + [2507]=350, [2508]=5, [2509]=82, [2510]=8, [2511]=264, [2512]=0, + [2515]=0, [2516]=0, [2519]=0, [2520]=4925, [2521]=6179, [2522]=4509, + [2523]=5657, [2524]=3838, [2525]=5297, [2526]=3867, [2527]=5871, [2528]=10367, + [2529]=13006, [2530]=10443, [2531]=11233, [2532]=10521, [2533]=12221, [2534]=9086, + [2535]=12311, [2540]=53622, [2541]=53622, [2545]=1796, [2546]=55, [2547]=7, + [2549]=5016, [2550]=1, [2551]=1, [2552]=1, [2553]=25, [2555]=40, + [2556]=40, [2557]=4, [2558]=3, [2559]=4, [2562]=575, [2564]=6817, + [2565]=3113, [2566]=1322, [2567]=2509, [2568]=31, [2569]=87, [2570]=11, + [2571]=16, [2572]=99, [2575]=25, [2576]=75, [2577]=75, [2578]=224, + [2579]=37, [2580]=67, [2581]=20, [2582]=358, [2583]=359, [2584]=253, + [2585]=638, [2586]=1, [2587]=200, [2589]=13, [2590]=5, [2591]=5, + [2592]=33, [2593]=37, [2594]=375, [2595]=500, [2596]=30, [2598]=30, + [2601]=100, [2604]=12, [2605]=25, [2608]=63, [2612]=32, [2613]=121, + [2614]=232, [2615]=1018, [2616]=531, [2617]=2198, [2618]=5327, [2620]=3013, + [2621]=2357, [2622]=2603, [2623]=3554, [2624]=3852, [2632]=605, [2633]=25, + [2635]=16, [2642]=33, [2643]=28, [2644]=11, [2645]=11, [2646]=31, + [2648]=58, [2649]=1, [2650]=3, [2651]=3, [2652]=7, [2653]=3, + [2654]=2, [2656]=9, [2657]=875, [2662]=8750, [2663]=8750, [2664]=1073, + [2665]=5, [2672]=4, [2673]=10, [2674]=12, [2675]=11, [2677]=15, + [2678]=0, [2679]=5, [2680]=10, [2681]=6, [2682]=25, [2683]=25, + [2684]=20, [2685]=75, [2686]=12, [2687]=25, [2688]=12, [2690]=7, + [2691]=10, [2692]=10, [2693]=20, [2694]=539, [2695]=3, [2697]=100, + [2698]=100, [2699]=200, [2700]=100, [2701]=400, [2703]=2, [2704]=2, + [2705]=2, [2706]=2, [2707]=2, [2708]=2, [2709]=2, [2710]=2, + [2711]=3, [2714]=238, [2715]=3, [2716]=2, [2717]=2, [2718]=2, + [2721]=1982, [2723]=12, [2725]=375, [2728]=375, [2730]=375, [2732]=375, + [2734]=375, [2735]=375, [2738]=375, [2740]=375, [2742]=375, [2744]=375, + [2745]=375, [2748]=375, [2749]=375, [2750]=375, [2751]=375, [2754]=13, + [2763]=240, [2764]=440, [2765]=811, [2766]=1564, [2770]=5, [2771]=25, + [2772]=150, [2773]=39, [2774]=28, [2775]=75, [2776]=500, [2777]=146, + [2778]=147, [2780]=374, [2781]=335, [2782]=751, [2783]=590, [2785]=1062, + [2786]=1173, [2787]=10, [2798]=25, [2799]=67, [2800]=1525, [2801]=104729, + [2802]=1625, [2805]=2352, [2807]=2452, [2808]=2364, [2809]=3, [2810]=3, + [2813]=3, [2815]=19778, [2816]=7324, [2817]=201, [2818]=363, [2819]=3830, + [2820]=4662, [2821]=1117, [2822]=1402, [2823]=1618, [2824]=32032, [2825]=14722, + [2827]=3, [2835]=2, [2836]=15, [2838]=60, [2840]=10, [2841]=50, + [2842]=100, [2844]=106, [2845]=109, [2847]=110, [2848]=1119, [2849]=1269, + [2850]=1439, [2851]=56, [2852]=67, [2853]=17, [2854]=373, [2857]=312, + [2862]=3, [2863]=10, [2864]=630, [2865]=962, [2866]=1312, [2867]=629, + [2868]=807, [2869]=1831, [2870]=2935, [2871]=40, [2877]=8524, [2878]=5143, + [2879]=3121, [2880]=25, [2881]=150, [2882]=300, [2883]=375, [2886]=5, + [2887]=5, [2888]=10, [2889]=60, [2890]=5, [2892]=30, [2893]=55, + [2894]=12, [2895]=12, [2896]=25, [2898]=32, [2899]=169, [2900]=89, + [2901]=16, [2902]=1305, [2903]=515, [2904]=595, [2905]=53, [2906]=1810, + [2907]=1755, [2908]=1409, [2910]=1257, [2911]=524, [2912]=6731, [2913]=1153, + [2915]=55289, [2916]=4405, [2917]=665, [2919]=1000, [2920]=50, [2921]=125, + [2922]=250, [2923]=500, [2924]=16, [2927]=50, [2928]=5, [2929]=2, + [2930]=12, [2931]=25, [2932]=50, [2933]=3750, [2934]=7, [2940]=43, + [2941]=3552, [2942]=3663, [2943]=537, [2946]=20, [2947]=10, [2949]=1039, + [2950]=3476, [2951]=656, [2953]=2321, [2954]=2485, [2955]=3449, [2957]=119, + [2958]=94, [2959]=32, [2960]=21, [2961]=155, [2962]=120, [2963]=41, + [2964]=36, [2965]=189, [2966]=146, [2967]=66, [2968]=33, [2969]=381, + [2970]=333, [2971]=108, [2972]=72, [2973]=484, [2974]=392, [2975]=128, + [2976]=171, [2977]=476, [2978]=415, [2979]=157, [2980]=209, [2981]=648, + [2982]=565, [2983]=280, [2984]=215, [2985]=822, [2986]=718, [2987]=408, + [2988]=314, [2989]=1001, [2990]=933, [2991]=705, [2992]=408, [2996]=40, + [2997]=100, [3000]=119, [3002]=1000, [3003]=50, [3004]=125, [3005]=250, + [3006]=500, [3008]=43, [3010]=101, [3011]=2846, [3012]=50, [3013]=25, + [3018]=781, [3019]=423, [3020]=2655, [3021]=2421, [3022]=1085, [3023]=754, + [3024]=1419, [3025]=3695, [3026]=762, [3027]=1269, [3028]=3086, [3030]=0, + [3033]=0, [3034]=0, [3036]=515, [3037]=4814, [3039]=1610, [3040]=940, + [3041]=3769, [3042]=4577, [3044]=3, [3045]=1573, [3047]=1052, [3048]=1919, + [3049]=2119, [3053]=2140, [3055]=1526, [3056]=1532, [3057]=903, [3058]=682, + [3065]=658, [3066]=497, [3067]=1275, [3069]=1412, [3070]=11, [3071]=231, + [3072]=1207, [3073]=1211, [3074]=476, [3075]=15073, [3076]=636, [3078]=2314, + [3079]=297, [3087]=11, [3088]=1050, [3089]=325, [3090]=10, [3091]=225, + [3092]=2750, [3093]=1325, [3094]=225, [3095]=1050, [3096]=150, [3097]=75, + [3098]=1750, [3099]=5000, [3100]=2025, [3101]=5000, [3102]=2025, [3103]=466, + [3107]=45, [3108]=90, [3111]=10, [3113]=20, [3114]=1250, [3115]=1675, + [3116]=150, [3118]=5000, [3119]=2425, [3120]=1050, [3121]=3250, [3122]=550, + [3123]=1325, [3124]=2425, [3125]=187, [3126]=250, [3127]=750, [3129]=1675, + [3130]=1250, [3131]=20, [3132]=2425, [3133]=2000, [3134]=20, [3135]=45, + [3137]=90, [3138]=725, [3139]=3250, [3140]=625, [3141]=225, [3142]=187, + [3143]=50, [3144]=450, [3146]=1750, [3151]=87, [3152]=28, [3153]=33, + [3154]=1094, [3158]=60, [3160]=470, [3161]=294, [3164]=33, [3166]=407, + [3167]=68, [3168]=5, [3169]=18, [3170]=47, [3171]=6, [3172]=18, + [3173]=15, [3174]=16, [3175]=100, [3176]=75, [3177]=50, [3179]=125, + [3180]=168, [3181]=23, [3182]=387, [3184]=1394, [3185]=8088, [3186]=4436, + [3187]=14870, [3188]=879, [3189]=99, [3190]=99, [3191]=3857, [3192]=495, + [3193]=2072, [3194]=2495, [3195]=1372, [3196]=1377, [3197]=9295, [3198]=2741, + [3199]=2262, [3200]=19, [3201]=4496, [3202]=584, [3203]=5436, [3204]=751, + [3205]=229, [3206]=4582, [3207]=87, [3208]=25378, [3209]=6783, [3210]=5626, + [3211]=373, [3212]=929, [3213]=89, [3214]=33, [3216]=55, [3217]=134, + [3220]=40, [3222]=1203, [3223]=306, [3224]=28, [3225]=109, [3227]=2633, + [3228]=1098, [3229]=424, [3230]=768, [3231]=1266, [3233]=212, [3235]=412, + [3239]=3, [3240]=10, [3241]=40, [3259]=5, [3260]=6, [3261]=7, + [3262]=10, [3263]=4, [3267]=25, [3268]=25, [3269]=25, [3270]=10, + [3272]=13, [3273]=15, [3274]=7, [3275]=5, [3276]=15, [3277]=30, + [3278]=1720, [3279]=105, [3280]=33, [3281]=56, [3282]=235, [3283]=295, + [3284]=88, [3285]=36, [3286]=59, [3287]=199, [3288]=250, [3289]=57, + [3290]=48, [3291]=202, [3292]=203, [3293]=11, [3294]=11, [3295]=10, + [3296]=10, [3299]=48, [3300]=9, [3301]=102, [3302]=319, [3303]=70, + [3304]=106, [3305]=492, [3306]=653, [3307]=215, [3308]=144, [3309]=333, + [3310]=442, [3311]=152, [3312]=70, [3313]=519, [3314]=171, [3315]=454, + [3319]=110, [3320]=22, [3321]=41, [3322]=11, [3323]=15, [3324]=1119, + [3325]=140, [3326]=3, [3327]=141, [3328]=46, [3329]=179, [3330]=281, + [3331]=101, [3332]=48, [3334]=68, [3335]=46, [3336]=3986, [3340]=31, + [3341]=1621, [3342]=137, [3343]=450, [3344]=145, [3345]=2720, [3346]=3, + [3350]=2, [3351]=2, [3352]=1250, [3355]=50, [3356]=30, [3357]=75, + [3358]=175, [3360]=625, [3361]=3, [3362]=2, [3363]=1, [3364]=3, + [3365]=3, [3366]=3, [3367]=2, [3368]=2, [3369]=25, [3370]=10, + [3371]=1, [3372]=10, [3373]=14, [3374]=45, [3375]=30, [3376]=86, + [3377]=99, [3378]=132, [3379]=152, [3380]=365, [3381]=303, [3382]=10, + [3383]=100, [3384]=20, [3385]=30, [3386]=35, [3387]=30, [3388]=40, + [3389]=40, [3390]=35, [3391]=20, [3392]=1663, [3393]=250, [3394]=250, + [3395]=250, [3396]=250, [3399]=81, [3400]=2761, [3401]=81, [3402]=602, + [3403]=321, [3404]=181, [3413]=3229, [3414]=4028, [3415]=3598, [3416]=2213, + [3417]=4629, [3419]=125, [3420]=1250, [3421]=50, [3422]=500, [3423]=5000, + [3424]=125000, [3426]=1000, [3427]=1500, [3428]=100, [3429]=586, [3430]=11026, + [3431]=1335, [3432]=2, [3433]=2, [3434]=3, [3435]=19, [3437]=23, + [3438]=28, [3439]=30, [3440]=640, [3442]=44, [3443]=138, [3444]=90, + [3445]=226, [3446]=592, [3447]=34, [3448]=6, [3449]=207, [3450]=443, + [3451]=607, [3452]=2322, [3453]=46, [3454]=105, [3455]=188, [3456]=6375, + [3457]=444, [3458]=334, [3461]=1005, [3462]=2283, [3463]=9, [3464]=8, + [3465]=9, [3466]=500, [3469]=49, [3470]=5, [3471]=142, [3472]=182, + [3473]=299, [3474]=216, [3475]=26018, [3478]=10, [3480]=319, [3481]=1284, + [3482]=1317, [3483]=965, [3484]=1767, [3485]=1295, [3486]=100, [3487]=1498, + [3488]=613, [3489]=937, [3490]=2731, [3491]=2741, [3492]=4552, [3493]=3426, + [3494]=2, [3511]=267, [3522]=2, [3526]=1, [3527]=2, [3528]=1, + [3529]=1, [3530]=28, [3531]=57, [3533]=1, [3535]=1, [3536]=1, + [3537]=1, [3538]=1, [3541]=1, [3542]=1, [3545]=1, [3546]=1, + [3547]=1, [3555]=1032, [3556]=1285, [3558]=1044, [3559]=473, [3560]=2102, + [3561]=895, [3562]=823, [3563]=557, [3565]=280, [3566]=2023, [3567]=922, + [3569]=1262, [3570]=839, [3571]=2118, [3572]=972, [3573]=212, [3574]=212, + [3575]=200, [3576]=35, [3577]=600, [3578]=339, [3581]=1046, [3582]=474, + [3583]=57, [3585]=953, [3586]=1034, [3587]=2862, [3588]=2873, [3589]=115, + [3590]=115, [3591]=419, [3592]=420, [3593]=1095, [3594]=1099, [3595]=4, + [3596]=4, [3597]=216, [3598]=217, [3599]=4, [3600]=4, [3602]=29, + [3603]=29, [3604]=500, [3605]=500, [3606]=29, [3607]=29, [3608]=500, + [3609]=25, [3610]=50, [3611]=500, [3612]=500, [3641]=15, [3642]=22, + [3643]=71, [3644]=47, [3645]=189, [3647]=433, [3648]=73, [3649]=115, + [3650]=68, [3651]=435, [3652]=364, [3653]=438, [3654]=366, [3655]=1022, + [3656]=2116, [3661]=9, [3662]=25, [3663]=125, [3664]=100, [3665]=150, + [3666]=100, [3667]=25, [3669]=195, [3670]=70, [3671]=201, [3673]=45, + [3674]=95, [3675]=56, [3676]=106, [3678]=100, [3679]=100, [3680]=400, + [3681]=400, [3682]=400, [3683]=400, [3685]=71, [3696]=2, [3699]=2, + [3702]=498, [3703]=36, [3712]=87, [3713]=40, [3719]=1027, [3722]=213, + [3723]=68, [3724]=81, [3725]=166, [3726]=125, [3727]=125, [3728]=300, + [3729]=300, [3730]=45, [3731]=55, [3732]=801, [3733]=1269, [3734]=400, + [3735]=450, [3736]=500, [3737]=550, [3738]=3439, [3739]=1250, [3740]=2452, + [3741]=922, [3742]=2862, [3743]=2451, [3747]=1092, [3748]=1304, [3749]=1320, + [3750]=2209, [3751]=2661, [3752]=745, [3753]=1635, [3754]=1492, [3755]=5447, + [3758]=1817, [3759]=1216, [3760]=1500, [3761]=3306, [3762]=1000, [3763]=6761, + [3764]=3181, [3765]=4811, [3766]=30, [3767]=23, [3769]=13, [3770]=25, + [3771]=50, [3774]=2, [3775]=13, [3776]=175, [3777]=10, [3778]=1541, + [3779]=2835, [3780]=1877, [3781]=3125, [3782]=3451, [3783]=3048, [3784]=4208, + [3785]=3650, [3786]=3957, [3787]=4289, [3788]=3000, [3789]=2000, [3790]=2000, + [3791]=3000, [3792]=521, [3793]=648, [3794]=774, [3795]=981, [3796]=493, + [3797]=1692, [3798]=902, [3799]=1460, [3800]=1068, [3801]=1031, [3802]=627, + [3803]=1086, [3804]=765, [3805]=1049, [3806]=1271, [3807]=1162, [3808]=770, + [3809]=962, [3810]=1145, [3811]=1064, [3812]=971, [3813]=1772, [3814]=1891, + [3815]=1475, [3816]=2102, [3817]=1585, [3818]=125, [3819]=100, [3820]=100, + [3821]=150, [3822]=6481, [3823]=100, [3824]=150, [3825]=110, [3826]=105, + [3827]=120, [3828]=150, [3829]=150, [3830]=500, [3831]=550, [3832]=550, + [3833]=24, [3834]=32, [3835]=1771, [3836]=3053, [3837]=4405, [3840]=2571, + [3841]=3106, [3842]=2906, [3843]=3882, [3844]=5658, [3845]=6558, [3846]=3937, + [3847]=6183, [3848]=1426, [3849]=5468, [3850]=7304, [3851]=6258, [3852]=8360, + [3853]=10153, [3854]=14120, [3855]=11248, [3856]=14221, [3857]=125, [3858]=250, + [3859]=60, [3860]=400, [3861]=125, [3864]=800, [3866]=1000, [3867]=950, + [3868]=1250, [3869]=1250, [3870]=750, [3871]=850, [3872]=800, [3873]=1100, + [3874]=1100, [3875]=1250, [3882]=13, [3889]=1584, [3890]=2040, [3891]=2456, + [3892]=4388, [3893]=5504, [3894]=5997, [3899]=25, [3902]=1317, [3914]=6250, + [3927]=150, [3928]=250, [3931]=185, [3936]=985, [3937]=2018, [3938]=1189, + [3939]=2256, [3940]=1630, [3941]=2598, [3942]=2636, [3943]=2244, [3944]=2277, + [3945]=3852, [3946]=2431, [3947]=3258, [3948]=3092, [3949]=6517, [3950]=4157, + [3951]=5897, [3952]=5109, [3953]=6327, [3954]=4133, [3955]=5375, [3956]=3776, + [3957]=9216, [3958]=7649, [3959]=8843, [3961]=1503, [3962]=3079, [3963]=1402, + [3964]=2127, [3965]=2221, [3966]=2625, [3967]=2903, [3968]=3331, [3969]=3256, + [3970]=4296, [3971]=3811, [3972]=3891, [3973]=3657, [3974]=5486, [3975]=3896, + [3976]=6978, [3977]=4738, [3978]=9560, [3979]=5802, [3980]=6844, [3981]=6137, + [3982]=11175, [3983]=260, [3984]=10209, [3986]=5332, [3987]=6837, [3988]=12924, + [3989]=3154, [3990]=8237, [3991]=15134, [3992]=3082, [3993]=4940, [3994]=4404, + [3995]=6284, [3996]=4659, [3997]=7929, [3998]=5656, [3999]=7109, [4000]=1797, + [4001]=3170, [4002]=2463, [4003]=4090, [4004]=2357, [4005]=3477, [4006]=2434, + [4007]=3696, [4008]=5494, [4009]=9161, [4010]=6409, [4011]=10687, [4012]=6781, + [4013]=11759, [4014]=11349, [4015]=10747, [4017]=6910, [4018]=6372, [4019]=8336, + [4020]=11189, [4021]=7198, [4022]=12059, [4023]=6215, [4024]=9821, [4025]=5070, + [4026]=4362, [4030]=4000, [4031]=4000, [4032]=5000, [4033]=5000, [4035]=1988, + [4036]=681, [4037]=1821, [4038]=3562, [4039]=2215, [4040]=1347, [4041]=3149, + [4042]=2107, [4043]=1958, [4044]=4584, [4045]=2683, [4046]=6783, [4047]=3959, + [4048]=1767, [4049]=888, [4050]=2374, [4051]=1669, [4052]=2699, [4054]=3625, + [4055]=2481, [4057]=4032, [4058]=6056, [4059]=2233, [4060]=5648, [4061]=3306, + [4062]=8359, [4063]=3596, [4064]=2987, [4065]=5311, [4066]=4845, [4067]=6381, + [4068]=6918, [4069]=11900, [4070]=7527, [4071]=2939, [4072]=1340, [4073]=2230, + [4074]=4785, [4075]=1984, [4076]=3301, [4077]=3298, [4078]=4672, [4079]=6753, + [4080]=7469, [4082]=11699, [4083]=4328, [4084]=10136, [4086]=6386, [4087]=8722, + [4088]=18523, [4089]=15060, [4090]=20736, [4091]=22479, [4092]=1296, [4093]=713, + [4096]=608, [4097]=305, [4099]=1131, [4100]=23, [4101]=26, [4102]=33, + [4107]=2136, [4108]=5403, [4109]=4393, [4110]=11999, [4111]=12042, [4112]=4182, + [4113]=4853, [4114]=5826, [4115]=5783, [4116]=12337, [4117]=3119, [4118]=7313, + [4119]=7271, [4120]=5031, [4121]=1380, [4122]=8384, [4123]=3804, [4124]=3991, + [4125]=3142, [4126]=6396, [4127]=8073, [4128]=14587, [4129]=9444, [4130]=2171, + [4131]=3815, [4132]=2681, [4133]=1794, [4134]=26994, [4135]=1130, [4136]=6021, + [4137]=4010, [4138]=10140, [4139]=2749, [4140]=1875, [4141]=20, [4142]=750, + [4143]=26994, [4144]=2000, [4145]=2025, [4146]=5000, [4147]=1050, [4148]=1325, + [4149]=3000, [4150]=2425, [4151]=725, [4152]=2750, [4153]=1325, [4154]=5000, + [4155]=5000, [4157]=125, [4158]=5500, [4159]=50, [4160]=4500, [4161]=550, + [4162]=4250, [4164]=2425, [4165]=4500, [4166]=50, [4167]=5000, [4168]=450, + [4169]=1250, [4170]=2000, [4171]=1750, [4172]=3750, [4173]=1750, [4174]=3750, + [4175]=2750, [4176]=3750, [4177]=2500, [4178]=3250, [4179]=2500, [4180]=3000, + [4181]=4250, [4182]=4250, [4183]=3750, [4184]=4500, [4185]=5000, [4186]=2500, + [4187]=5000, [4188]=5000, [4189]=2750, [4190]=1025, [4194]=520, [4195]=727, + [4196]=1944, [4197]=2765, [4198]=250, [4199]=750, [4200]=1050, [4201]=2025, + [4202]=1325, [4203]=1500, [4204]=1750, [4205]=3250, [4206]=1250, [4207]=2000, + [4208]=2500, [4209]=2000, [4210]=325, [4211]=325, [4212]=2500, [4213]=2500, + [4214]=2425, [4215]=725, [4216]=5000, [4217]=3750, [4218]=1500, [4219]=5000, + [4220]=2000, [4221]=5000, [4223]=3750, [4225]=2025, [4226]=4500, [4228]=1050, + [4230]=2750, [4231]=110, [4232]=125, [4233]=200, [4234]=150, [4235]=200, + [4236]=225, [4237]=34, [4238]=200, [4239]=71, [4240]=300, [4241]=450, + [4242]=347, [4243]=461, [4244]=723, [4245]=2000, [4246]=193, [4247]=1049, + [4248]=791, [4249]=703, [4250]=705, [4251]=1199, [4252]=1457, [4253]=962, + [4254]=1071, [4255]=2366, [4256]=3477, [4257]=1311, [4258]=2055, [4259]=1934, + [4260]=2559, [4261]=31, [4262]=2652, [4263]=94, [4264]=2804, [4265]=650, + [4266]=725, [4267]=750, [4268]=1050, [4269]=2425, [4270]=2025, [4271]=2425, + [4272]=2425, [4273]=325, [4274]=3750, [4275]=4250, [4276]=2500, [4277]=3750, + [4278]=25, [4279]=3250, [4280]=1325, [4281]=2425, [4282]=3750, [4283]=5000, + [4284]=4500, [4285]=3750, [4286]=5000, [4287]=2500, [4288]=4250, [4289]=12, + [4290]=715, [4291]=125, [4292]=200, [4293]=162, [4294]=400, [4295]=375, + [4296]=525, [4297]=500, [4298]=162, [4299]=500, [4300]=700, [4301]=875, + [4302]=146, [4303]=398, [4304]=300, [4305]=600, [4306]=150, [4307]=29, + [4308]=45, [4309]=226, [4310]=180, [4311]=475, [4312]=237, [4313]=416, + [4314]=331, [4315]=896, [4316]=743, [4317]=1076, [4318]=610, [4319]=815, + [4320]=979, [4321]=1120, [4322]=1810, [4323]=1999, [4324]=1874, [4325]=2272, + [4326]=2496, [4327]=3788, [4328]=1524, [4329]=2120, [4330]=250, [4331]=526, + [4332]=500, [4333]=1200, [4334]=550, [4335]=1500, [4336]=1500, [4337]=750, + [4338]=250, [4339]=1250, [4340]=87, [4341]=125, [4342]=625, [4343]=60, + [4344]=11, [4345]=100, [4346]=100, [4347]=150, [4348]=175, [4349]=175, + [4350]=200, [4351]=225, [4352]=275, [4353]=300, [4354]=350, [4355]=375, + [4356]=375, [4357]=4, [4358]=30, [4359]=12, [4360]=60, [4361]=120, + [4362]=187, [4363]=50, [4364]=12, [4365]=75, [4366]=75, [4367]=150, + [4368]=408, [4369]=1179, [4370]=175, [4371]=200, [4372]=1800, [4373]=722, + [4374]=200, [4375]=115, [4376]=200, [4377]=150, [4378]=350, [4379]=2357, + [4380]=500, [4381]=600, [4382]=600, [4383]=3183, [4384]=1000, [4385]=1410, + [4386]=175, [4387]=400, [4388]=1000, [4389]=750, [4390]=500, [4391]=4000, + [4392]=2500, [4393]=3172, [4394]=750, [4395]=1600, [4396]=6000, [4397]=5000, + [4398]=900, [4399]=50, [4400]=500, [4401]=100, [4402]=250, [4403]=2000, + [4404]=25, [4405]=125, [4406]=600, [4407]=1200, [4408]=162, [4409]=200, + [4410]=250, [4411]=275, [4412]=375, [4413]=450, [4414]=462, [4415]=550, + [4416]=600, [4417]=675, [4419]=112, [4421]=100, [4422]=112, [4424]=100, + [4425]=125, [4426]=125, [4428]=331, [4430]=4307, [4434]=572, [4436]=331, + [4437]=1855, [4438]=1692, [4439]=1788, [4443]=3472, [4444]=1490, [4445]=2068, + [4446]=3594, [4447]=901, [4448]=1991, [4449]=3332, [4454]=3800, [4455]=3096, + [4456]=1553, [4457]=300, [4459]=150, [4460]=175, [4461]=208, [4462]=1425, + [4463]=953, [4464]=2380, [4465]=1585, [4470]=9, [4471]=33, [4474]=4207, + [4476]=2734, [4477]=4390, [4478]=10176, [4479]=178, [4480]=185, [4481]=176, + [4496]=125, [4497]=5000, [4498]=625, [4499]=25000, [4500]=8750, [4504]=2222, + [4505]=1974, [4507]=9938, [4508]=9351, [4509]=2794, [4511]=11256, [4534]=1173, + [4535]=882, [4536]=1, [4537]=6, [4538]=25, [4539]=50, [4540]=1, + [4541]=6, [4542]=25, [4543]=4098, [4544]=50, [4545]=2202, [4546]=533, + [4547]=8319, [4548]=15028, [4549]=2092, [4550]=2092, [4552]=530, [4553]=411, + [4554]=708, [4555]=155, [4556]=903, [4557]=225, [4558]=1565, [4560]=37, + [4561]=309, [4562]=298, [4563]=110, [4564]=610, [4565]=38, [4566]=631, + [4567]=1049, [4568]=1694, [4569]=612, [4570]=922, [4571]=979, [4575]=1487, + [4576]=1184, [4577]=356, [4580]=787, [4581]=862, [4582]=745, [4583]=812, + [4584]=937, [4585]=583, [4586]=713, [4587]=807, [4588]=900, [4589]=530, + [4590]=655, [4591]=413, [4592]=1, [4593]=4, [4594]=6, [4595]=75, + [4596]=25, [4597]=250, [4598]=212, [4599]=100, [4600]=85, [4601]=100, + [4602]=100, [4603]=4, [4604]=1, [4605]=6, [4606]=25, [4607]=50, + [4608]=100, [4609]=250, [4611]=50, [4616]=3, [4623]=375, [4624]=550, + [4625]=250, [4632]=50, [4633]=70, [4634]=87, [4636]=110, [4637]=150, + [4638]=200, [4639]=162, [4643]=2748, [4652]=10126, [4653]=7178, [4655]=71, + [4656]=1, [4658]=23, [4659]=23, [4660]=324, [4661]=929, [4662]=24, + [4663]=23, [4665]=22, [4666]=26, [4668]=32, [4669]=41, [4671]=32, + [4672]=28, [4674]=32, [4675]=35, [4676]=278, [4677]=70, [4678]=108, + [4680]=91, [4681]=109, [4683]=92, [4684]=61, [4686]=67, [4687]=67, + [4689]=106, [4690]=71, [4692]=68, [4693]=86, [4694]=515, [4695]=327, + [4696]=5537, [4697]=435, [4698]=301, [4699]=221, [4700]=379, [4701]=252, + [4704]=3, [4705]=1449, [4706]=687, [4707]=881, [4708]=461, [4709]=982, + [4710]=561, [4711]=1112, [4712]=1228, [4713]=926, [4714]=681, [4715]=1026, + [4716]=1658, [4717]=2014, [4718]=2223, [4719]=1676, [4720]=1234, [4721]=2809, + [4722]=2118, [4723]=1703, [4724]=1763, [4725]=4736, [4726]=2340, [4727]=2791, + [4729]=3036, [4731]=3824, [4732]=2632, [4733]=7957, [4734]=4208, [4735]=3621, + [4736]=2616, [4737]=5743, [4738]=3294, [4741]=5438, [4743]=5430, [4744]=1986, + [4745]=3230, [4746]=4324, [4750]=4, [4756]=5, [4757]=4, [4763]=135, + [4765]=575, [4766]=481, [4767]=139, [4768]=139, [4771]=264, [4772]=70, + [4775]=28, [4776]=41, [4777]=1408, [4778]=1470, [4779]=13, [4780]=56, + [4781]=547, [4782]=415, [4784]=360, [4785]=469, [4786]=278, [4787]=577, + [4788]=526, [4789]=399, [4790]=831, [4791]=133, [4792]=655, [4793]=744, + [4794]=703, [4795]=705, [4796]=708, [4797]=852, [4798]=1166, [4799]=608, + [4800]=1221, [4810]=3351, [4813]=33, [4814]=6, [4816]=1503, [4817]=2462, + [4818]=2854, [4820]=1664, [4821]=1308, [4822]=1349, [4824]=3371, [4825]=4094, + [4826]=3087, [4827]=749, [4828]=684, [4829]=830, [4830]=1894, [4831]=1571, + [4832]=2099, [4833]=1731, [4835]=2110, [4836]=2000, [4837]=2000, [4838]=2000, + [4840]=142, [4852]=300, [4860]=741, [4861]=119, [4865]=5, [4867]=8, + [4872]=95, [4873]=15, [4874]=46, [4875]=13, [4876]=78, [4877]=10, + [4878]=56, [4879]=7, [4880]=86, [4906]=21, [4907]=13, [4908]=6, + [4909]=368, [4910]=7, [4911]=15, [4913]=7, [4914]=5, [4915]=7, + [4916]=9, [4917]=14, [4919]=5, [4920]=7, [4921]=12, [4922]=15, + [4923]=25, [4924]=25, [4925]=25, [4928]=20, [4929]=58, [4930]=250, + [4931]=134, [4932]=179, [4933]=48, [4935]=23, [4936]=23, [4937]=71, + [4938]=236, [4939]=395, [4940]=36, [4941]=11, [4942]=89, [4944]=72, + [4945]=37, [4946]=67, [4947]=325, [4948]=326, [4949]=1706, [4950]=780, + [4951]=13, [4952]=63, [4953]=88, [4954]=6, [4957]=250, [4958]=36, + [4959]=1, [4960]=12, [4961]=183, [4962]=20, [4963]=30, [4964]=503, + [4965]=149, [4967]=116, [4968]=90, [4969]=42, [4970]=54, [4971]=383, + [4972]=64, [4973]=35, [4974]=388, [4975]=6477, [4976]=5486, [4977]=11893, + [4978]=7976, [4979]=2587, [4980]=2209, [4981]=1000, [4982]=961, [4983]=16837, + [4984]=5630, [4987]=15584, [4988]=3657, [4989]=9058, [4990]=4546, [4991]=2, + [4993]=2, [4994]=2, [4998]=837, [4999]=1052, [5000]=962, [5001]=1038, + [5002]=1535, [5003]=1713, [5004]=1806, [5005]=1840, [5007]=1632, [5008]=1546, + [5009]=1696, [5010]=1803, [5011]=1912, [5013]=38, [5016]=2955, [5020]=3, + [5024]=10, [5028]=5537, [5029]=5282, [5042]=12, [5047]=1250, [5048]=12, + [5049]=12, [5051]=1, [5057]=1, [5060]=0, [5066]=21, [5069]=293, + [5071]=443, [5075]=25, [5079]=4642, [5081]=250, [5082]=25, [5083]=50, + [5092]=240, [5093]=247, [5094]=233, [5095]=3, [5105]=25, [5107]=139, + [5108]=1616, [5109]=223, [5110]=257, [5111]=320, [5112]=730, [5113]=250, + [5114]=96, [5115]=101, [5116]=303, [5117]=825, [5118]=71, [5119]=118, + [5120]=193, [5121]=162, [5122]=287, [5123]=117, [5124]=117, [5125]=155, + [5126]=500, [5127]=500, [5128]=202, [5129]=500, [5130]=500, [5131]=500, + [5132]=500, [5133]=300, [5134]=92, [5135]=142, [5136]=177, [5137]=217, + [5139]=20, [5140]=6, [5141]=150, [5142]=20, [5145]=87, [5147]=550, + [5148]=225, [5149]=225, [5150]=225, [5151]=1050, [5152]=725, [5153]=2425, + [5154]=1325, [5155]=75, [5156]=2425, [5158]=2425, [5160]=1675, [5163]=725, + [5172]=25, [5173]=25, [5180]=2777, [5181]=1778, [5182]=1769, [5183]=1575, + [5187]=2011, [5191]=2964, [5192]=1803, [5193]=940, [5194]=3079, [5195]=364, + [5196]=1830, [5197]=1597, [5198]=1660, [5199]=804, [5200]=2323, [5201]=3161, + [5202]=1147, [5205]=31, [5206]=37, [5207]=1081, [5208]=668, [5209]=771, + [5210]=1161, [5211]=1166, [5212]=672, [5213]=5218, [5214]=3935, [5215]=8656, + [5216]=11821, [5235]=41, [5236]=2878, [5237]=18, [5238]=7145, [5239]=7746, + [5240]=1244, [5241]=821, [5242]=623, [5243]=1312, [5244]=3465, [5245]=5091, + [5246]=3490, [5247]=7961, [5248]=6849, [5249]=8658, [5250]=2646, [5252]=1175, + [5253]=7952, [5254]=308, [5255]=685, [5256]=7227, [5257]=3159, [5258]=1, + [5259]=1, [5260]=1, [5261]=1, [5262]=1, [5263]=1, [5265]=1, + [5266]=11157, [5267]=59851, [5268]=218, [5269]=95, [5274]=1003, [5275]=175, + [5276]=7, [5277]=3, [5278]=6, [5279]=1436, [5280]=6, [5281]=6, + [5282]=2, [5283]=2, [5284]=6, [5285]=6, [5286]=2, [5287]=8, + [5288]=7, [5289]=7, [5291]=3, [5292]=2, [5293]=7, [5299]=359, + [5300]=7, [5301]=4, [5302]=929, [5303]=4, [5304]=2, [5305]=2, + [5306]=1710, [5309]=1038, [5310]=555, [5311]=601, [5312]=419, [5313]=650, + [5314]=528, [5315]=213, [5316]=1355, [5317]=1360, [5318]=1789, [5319]=323, + [5320]=372, [5321]=1484, [5322]=4016, [5323]=1632, [5324]=776, [5325]=498, + [5326]=776, [5327]=519, [5328]=205, [5329]=15, [5331]=236, [5332]=0, + [5337]=167, [5340]=919, [5341]=368, [5342]=88, [5343]=462, [5344]=562, + [5345]=705, [5346]=425, [5347]=3142, [5351]=403, [5355]=947, [5356]=2594, + [5357]=2221, [5361]=16, [5362]=18, [5363]=20, [5364]=27, [5367]=22, + [5368]=48, [5369]=32, [5370]=37, [5371]=48, [5373]=72, [5374]=87, + [5375]=95, [5376]=66, [5377]=57, [5379]=1, [5392]=25, [5393]=32, + [5394]=6, [5395]=16, [5398]=13, [5399]=11, [5400]=17, [5401]=17, + [5402]=17, [5403]=17, [5404]=721, [5405]=7, [5419]=13, [5420]=227, + [5421]=650, [5422]=692, [5423]=2084, [5425]=419, [5426]=1686, [5427]=147, + [5428]=322, [5429]=137, [5430]=277, [5431]=155, [5432]=330, [5433]=138, + [5435]=272, [5439]=25, [5441]=250, [5443]=1067, [5444]=548, [5446]=13, + [5447]=20, [5448]=17, [5451]=22, [5457]=23, [5458]=27, [5459]=472, + [5465]=3, [5466]=8, [5467]=7, [5468]=12, [5469]=9, [5470]=28, + [5471]=30, [5472]=10, [5473]=10, [5474]=9, [5476]=3, [5477]=18, + [5478]=70, [5479]=125, [5480]=95, [5482]=10, [5483]=35, [5484]=60, + [5485]=100, [5486]=110, [5487]=200, [5488]=100, [5489]=300, [5491]=9, + [5495]=7, [5498]=200, [5500]=750, [5502]=2, [5503]=16, [5504]=22, + [5506]=71, [5507]=600, [5516]=317, [5517]=100, [5518]=150, [5523]=15, + [5524]=21, [5525]=20, [5526]=75, [5527]=95, [5528]=200, [5529]=125, + [5530]=125, [5532]=2, [5540]=2107, [5541]=3693, [5542]=370, [5543]=450, + [5565]=250, [5566]=105, [5567]=196, [5568]=4, [5569]=203, [5571]=250, + [5572]=250, [5573]=875, [5574]=875, [5575]=2500, [5576]=2500, [5577]=300, + [5578]=300, [5579]=32, [5580]=25, [5581]=32, [5586]=26, [5587]=512, + [5589]=41, [5590]=35, [5591]=67, [5592]=54, [5593]=116, [5595]=235, + [5596]=141, [5597]=2, [5598]=2, [5599]=2, [5600]=2, [5601]=22, + [5602]=63, [5603]=300, [5604]=380, [5605]=150, [5606]=23, [5608]=3463, + [5609]=447, [5610]=208, [5611]=452, [5612]=72, [5613]=2635, [5614]=6147, + [5615]=1776, [5616]=22738, [5617]=247, [5618]=86, [5622]=556, [5624]=2926, + [5626]=1717, [5627]=1379, [5629]=347, [5630]=348, [5631]=30, [5632]=85, + [5633]=150, [5634]=75, [5635]=45, [5636]=75, [5637]=75, [5640]=25, + [5641]=375, [5642]=450, [5643]=500, [5644]=10, [5647]=1000, [5648]=725, + [5649]=725, [5650]=725, [5654]=100, [5655]=0, [5656]=0, [5657]=250, + [5658]=87, [5660]=325, [5661]=1675, [5662]=3750, [5665]=0, [5666]=5000, + [5667]=1250, [5668]=0, [5670]=14750, [5671]=2500, [5672]=725, [5673]=2025, + [5674]=3750, [5676]=725, [5677]=1325, [5678]=3750, [5679]=3750, [5680]=725, + [5682]=4500, [5683]=8750, [5684]=1050, [5685]=5000, [5696]=20, [5697]=50, + [5698]=50, [5699]=250, [5700]=250, [5701]=325, [5702]=625, [5703]=750, + [5704]=750, [5705]=750, [5706]=750, [5707]=1675, [5708]=1250, [5709]=1750, + [5710]=1750, [5711]=2000, [5712]=2000, [5713]=2500, [5714]=3000, [5715]=5000, + [5716]=5000, [5719]=1050, [5720]=750, [5721]=3000, [5722]=4500, [5723]=3750, + [5724]=1050, [5725]=4250, [5726]=4250, [5727]=5000, [5728]=5000, [5729]=5000, + [5730]=10, [5739]=4935, [5740]=1, [5741]=111, [5742]=10770, [5743]=1803, + [5744]=386, [5745]=2, [5746]=2, [5747]=2, [5748]=609, [5749]=2664, + [5750]=641, [5751]=1028, [5752]=3109, [5753]=1884, [5754]=2538, [5755]=4127, + [5756]=10026, [5757]=1363, [5758]=250, [5759]=375, [5760]=500, [5761]=30, + [5762]=250, [5763]=700, [5764]=3000, [5765]=4000, [5766]=1338, [5767]=44, + [5768]=1250, [5769]=1250, [5770]=1807, [5771]=50, [5772]=125, [5773]=250, + [5774]=275, [5775]=350, [5776]=30, [5777]=30, [5778]=30, [5779]=30, + [5780]=260, [5781]=601, [5782]=3211, [5783]=2316, [5784]=75, [5785]=500, + [5786]=137, [5787]=150, [5788]=162, [5789]=700, [5812]=1129, [5813]=7087, + [5814]=2351, [5815]=5871, [5816]=405, [5817]=3226, [5818]=3239, [5819]=3201, + [5820]=1622, [5821]=900, [5822]=722, [5823]=5, [5829]=804, [5839]=1, + [5856]=3, [5857]=37, [5858]=37, [5859]=25, [5864]=0, [5870]=3, + [5871]=318, [5872]=0, [5873]=0, [5878]=92, [5936]=19, [5939]=20, + [5940]=153, [5941]=115, [5943]=840, [5944]=331, [5951]=41, [5956]=3, + [5957]=40, [5958]=829, [5961]=1089, [5962]=2794, [5963]=3151, [5964]=2609, + [5965]=2536, [5966]=2299, [5967]=209, [5968]=259, [5969]=781, [5970]=418, + [5971]=1136, [5972]=375, [5973]=162, [5974]=350, [5975]=532, [5976]=5000, + [5996]=95, [5997]=5, [6037]=1250, [6038]=312, [6039]=1250, [6040]=2497, + [6041]=1500, [6042]=250, [6043]=500, [6044]=450, [6045]=650, [6046]=950, + [6047]=1100, [6048]=100, [6049]=170, [6050]=300, [6051]=62, [6052]=300, + [6053]=200, [6054]=225, [6055]=375, [6056]=500, [6057]=500, [6058]=6, + [6059]=13, [6060]=4, [6061]=23, [6062]=28, [6063]=23, [6068]=375, + [6070]=6, [6076]=9, [6078]=15, [6084]=290, [6085]=243, [6087]=1727, + [6088]=2, [6092]=420, [6093]=4418, [6094]=1278, [6095]=688, [6096]=1, + [6097]=1, [6098]=1, [6116]=1, [6117]=1, [6118]=1, [6119]=1, + [6120]=1, [6121]=1, [6122]=1, [6123]=1, [6124]=1, [6125]=1, + [6126]=1, [6127]=1, [6129]=1, [6130]=1, [6131]=1, [6132]=250, + [6133]=225, [6134]=1, [6135]=1, [6136]=1, [6137]=1, [6138]=1, + [6139]=1, [6140]=1, [6144]=1, [6147]=37, [6148]=44, [6149]=120, + [6150]=22, [6171]=6, [6173]=7, [6174]=694, [6176]=15, [6177]=69, + [6179]=556, [6180]=423, [6182]=1, [6183]=2, [6185]=9, [6186]=1372, + [6187]=613, [6188]=305, [6189]=624, [6191]=615, [6194]=5308, [6195]=416, + [6197]=929, [6198]=1113, [6199]=650, [6200]=1223, [6201]=54, [6202]=35, + [6203]=121, [6204]=2065, [6205]=922, [6206]=555, [6211]=450, [6214]=595, + [6215]=601, [6216]=6, [6217]=24, [6218]=24, [6219]=144, [6220]=4893, + [6222]=25, [6223]=4797, [6224]=4, [6225]=3, [6226]=890, [6227]=3, + [6228]=3, [6229]=3, [6230]=1, [6231]=1, [6232]=2, [6233]=2, + [6234]=2, [6235]=2, [6236]=2, [6237]=2, [6238]=98, [6239]=160, + [6240]=161, [6241]=99, [6242]=243, [6243]=445, [6254]=2, [6256]=4, + [6260]=12, [6261]=250, [6263]=589, [6264]=884, [6266]=205, [6267]=149, + [6268]=234, [6269]=193, [6270]=50, [6271]=50, [6272]=75, [6273]=187, + [6274]=100, [6275]=200, [6282]=2243, [6289]=1, [6290]=1, [6291]=1, + [6292]=8, [6293]=33, [6294]=10, [6295]=12, [6296]=28, [6297]=7, + [6298]=130, [6299]=1, [6300]=443, [6301]=20, [6302]=628, [6303]=1, + [6304]=25, [6305]=25, [6306]=25, [6307]=1, [6308]=2, [6309]=100, + [6310]=150, [6311]=187, [6314]=1268, [6315]=2546, [6316]=3, [6317]=2, + [6318]=4802, [6319]=803, [6320]=2711, [6321]=1650, [6322]=4, [6323]=2611, + [6324]=1892, [6325]=10, [6326]=10, [6327]=12823, [6328]=100, [6329]=100, + [6330]=300, [6331]=9466, [6332]=1153, [6333]=1929, [6334]=3, [6335]=1581, + [6336]=305, [6337]=245, [6338]=125, [6339]=24, [6340]=875, [6341]=666, + [6342]=75, [6343]=87, [6344]=100, [6345]=100, [6346]=100, [6347]=100, + [6348]=125, [6349]=125, [6350]=469, [6351]=1, [6352]=1, [6353]=1, + [6354]=1, [6355]=1, [6356]=1, [6357]=1, [6358]=4, [6359]=5, + [6360]=2581, [6361]=2, [6362]=4, [6363]=250, [6364]=375, [6365]=180, + [6366]=1066, [6367]=3378, [6368]=100, [6369]=550, [6370]=10, [6371]=12, + [6372]=35, [6373]=35, [6374]=125, [6375]=250, [6376]=250, [6377]=250, + [6378]=292, [6379]=244, [6380]=654, [6381]=625, [6382]=523, [6383]=1519, + [6384]=250, [6385]=250, [6386]=2692, [6387]=1228, [6388]=2043, [6389]=2245, + [6390]=150, [6391]=150, [6392]=1000, [6393]=760, [6394]=1041, [6395]=1264, + [6396]=2814, [6397]=985, [6398]=989, [6399]=1639, [6400]=3089, [6401]=275, + [6402]=4699, [6403]=1771, [6404]=3242, [6405]=3167, [6406]=1970, [6407]=1198, + [6408]=1654, [6409]=1660, [6410]=1514, [6411]=7268, [6412]=4711, [6413]=2497, + [6414]=2055, [6415]=4565, [6416]=2728, [6417]=2536, [6418]=1697, [6419]=2299, + [6420]=3739, [6421]=2317, [6422]=3768, [6423]=7233, [6424]=3825, [6425]=4478, + [6426]=4162, [6427]=7016, [6428]=2795, [6429]=4544, [6430]=9098, [6431]=5436, + [6432]=3741, [6433]=4953, [6434]=1, [6438]=362, [6439]=237, [6440]=15812, + [6444]=228, [6445]=88, [6446]=532, [6447]=562, [6448]=1942, [6449]=701, + [6450]=200, [6451]=400, [6452]=28, [6453]=62, [6454]=225, [6455]=4, + [6456]=3, [6457]=4, [6458]=1, [6459]=934, [6460]=844, [6461]=1190, + [6463]=1527, [6465]=768, [6466]=413, [6467]=420, [6468]=658, [6469]=2240, + [6470]=20, [6471]=500, [6472]=3018, [6473]=1010, [6474]=137, [6475]=375, + [6476]=500, [6477]=274, [6478]=634, [6480]=713, [6481]=641, [6482]=728, + [6502]=1140, [6503]=762, [6504]=2933, [6505]=3680, [6506]=87, [6507]=44, + [6508]=34, [6509]=44, [6510]=54, [6511]=121, [6512]=191, [6513]=28, + [6514]=42, [6515]=37, [6517]=35, [6518]=70, [6519]=36, [6520]=41, + [6521]=47, [6522]=4, [6523]=284, [6524]=574, [6525]=1033, [6526]=2497, + [6527]=187, [6528]=358, [6529]=12, [6530]=25, [6531]=417, [6532]=62, + [6533]=62, [6536]=488, [6537]=210, [6538]=492, [6539]=162, [6540]=431, + [6541]=163, [6542]=219, [6543]=147, [6545]=675, [6546]=533, [6547]=267, + [6548]=233, [6549]=101, [6550]=204, [6551]=409, [6552]=601, [6553]=525, + [6554]=229, [6555]=130, [6556]=174, [6557]=302, [6558]=180, [6559]=533, + [6560]=615, [6561]=675, [6562]=508, [6563]=295, [6564]=512, [6565]=393, + [6566]=411, [6567]=1036, [6568]=920, [6569]=1044, [6570]=363, [6571]=1166, + [6572]=1323, [6573]=938, [6574]=568, [6575]=431, [6576]=572, [6577]=649, + [6578]=1302, [6579]=667, [6580]=1312, [6581]=422, [6582]=731, [6583]=425, + [6584]=1416, [6585]=558, [6586]=505, [6587]=1146, [6588]=458, [6589]=1056, + [6590]=1830, [6591]=1007, [6592]=2447, [6593]=922, [6594]=1120, [6595]=1124, + [6596]=2258, [6597]=1707, [6598]=2426, [6599]=2678, [6600]=867, [6601]=1437, + [6602]=873, [6603]=2334, [6604]=1200, [6605]=971, [6607]=2202, [6608]=1328, + [6609]=2148, [6610]=2156, [6611]=812, [6612]=1112, [6613]=744, [6614]=1120, + [6615]=824, [6616]=2203, [6617]=1371, [6618]=6, [6622]=55400, [6627]=2620, + [6628]=370, [6629]=630, [6630]=2067, [6631]=4053, [6632]=642, [6633]=2131, + [6641]=3675, [6642]=1880, [6643]=6, [6644]=6, [6645]=25, [6646]=31, + [6647]=40, [6651]=234, [6657]=5, [6659]=541, [6660]=36157, [6661]=115, + [6662]=95, [6663]=150, [6664]=892, [6665]=895, [6666]=830, [6667]=992, + [6668]=1245, [6669]=1378, [6670]=1672, [6671]=2068, [6672]=500, [6675]=1048, + [6676]=2245, [6677]=2401, [6678]=677, [6679]=4852, [6680]=2, [6681]=3124, + [6682]=1900, [6685]=1310, [6686]=2627, [6687]=9930, [6688]=2059, [6689]=8267, + [6690]=3346, [6691]=8866, [6692]=9788, [6693]=1816, [6694]=6309, [6695]=3002, + [6696]=5086, [6697]=2041, [6709]=545, [6710]=137, [6712]=12, [6713]=10, + [6714]=75, [6715]=21, [6716]=200, [6719]=1097, [6720]=3218, [6721]=1418, + [6722]=1331, [6723]=8155, [6725]=6105, [6726]=2388, [6727]=2996, [6729]=7187, + [6730]=496, [6731]=871, [6732]=2421, [6733]=1145, [6734]=62, [6735]=150, + [6736]=450, [6737]=2805, [6738]=7040, [6739]=2991, [6740]=1000, [6741]=3014, + [6742]=2830, [6743]=1462, [6744]=1190, [6745]=2239, [6746]=7075, [6747]=5015, + [6748]=897, [6749]=897, [6750]=897, [6751]=1411, [6752]=1601, [6754]=500, + [6756]=1375, [6757]=5567, [6773]=7366, [6774]=2885, [6780]=1560, [6784]=2031, + [6786]=59, [6787]=466, [6788]=3331, [6789]=4012, [6790]=1540, [6791]=3464, + [6792]=4738, [6793]=3157, [6794]=2641, [6795]=500, [6796]=750, [6797]=6362, + [6798]=6386, [6801]=4456, [6802]=18255, [6803]=4885, [6804]=11262, [6806]=10209, + [6807]=62, [6811]=25, [6826]=548, [6827]=150, [6828]=6268, [6829]=18714, + [6830]=23476, [6831]=17522, [6832]=3769, [6833]=500, [6834]=1, [6835]=504, + [6836]=1, [6837]=1, [6886]=18, [6887]=5, [6888]=10, [6889]=4, + [6890]=6, [6891]=6, [6892]=62, [6898]=4132, [6899]=5382, [6900]=4685, + [6901]=1523, [6902]=877, [6903]=1549, [6904]=4665, [6905]=4010, [6906]=1275, + [6907]=1872, [6908]=522, [6909]=7155, [6910]=2298, [6911]=1442, [6946]=2, + [6947]=5, [6949]=20, [6950]=30, [6951]=75, [6953]=7459, [6966]=693, + [6967]=696, [6968]=698, [6969]=701, [6970]=906, [6971]=2221, [6972]=3242, + [6973]=2465, [6974]=1497, [6975]=16766, [6976]=17264, [6977]=17325, [6978]=672, + [6979]=675, [6980]=677, [6981]=680, [6982]=683, [6983]=685, [6984]=688, + [6985]=690, [6986]=50, [6987]=13, [6998]=774, [7000]=650, [7001]=3535, + [7002]=3028, [7003]=981, [7004]=984, [7005]=16, [7009]=16, [7010]=4, + [7011]=20, [7012]=1, [7013]=1580, [7026]=22, [7027]=1125, [7046]=1494, + [7047]=824, [7048]=1244, [7049]=914, [7050]=1257, [7051]=2696, [7052]=1488, + [7053]=2240, [7054]=4700, [7055]=1503, [7056]=2314, [7057]=2323, [7058]=3329, + [7059]=2781, [7060]=2791, [7061]=2017, [7062]=3899, [7063]=4741, [7064]=2569, + [7065]=2398, [7067]=400, [7068]=400, [7069]=400, [7070]=400, [7071]=100, + [7072]=150, [7073]=6, [7074]=4, [7075]=400, [7076]=400, [7077]=400, + [7078]=400, [7079]=400, [7080]=400, [7081]=400, [7082]=400, [7084]=350, + [7085]=350, [7086]=375, [7087]=300, [7088]=1250, [7089]=375, [7090]=250, + [7091]=250, [7092]=250, [7093]=200, [7094]=174, [7095]=32, [7096]=5, + [7097]=1, [7098]=6, [7099]=6, [7100]=7, [7101]=5, [7106]=1234, + [7107]=1858, [7108]=209, [7109]=74, [7110]=2069, [7111]=3329, [7112]=4547, + [7113]=6707, [7114]=250, [7115]=683, [7116]=685, [7117]=688, [7118]=690, + [7120]=895, [7129]=1546, [7130]=2328, [7132]=2338, [7133]=3098, [7148]=21, + [7166]=194, [7170]=692, [7187]=979, [7188]=1485, [7189]=4712, [7191]=275, + [7192]=300, [7228]=25, [7229]=285, [7230]=3103, [7276]=34, [7277]=28, + [7278]=25, [7279]=25, [7280]=162, [7281]=84, [7282]=599, [7283]=519, + [7284]=586, [7285]=588, [7286]=25, [7287]=100, [7288]=125, [7289]=162, + [7290]=400, [7296]=56, [7298]=468, [7307]=62, [7326]=696, [7327]=698, + [7328]=701, [7329]=703, [7330]=3923, [7331]=4330, [7332]=6692, [7334]=1544, + [7335]=1937, [7336]=1509, [7337]=250000, [7338]=2500, [7339]=62500, [7340]=125000, + [7341]=12500, [7342]=25000, [7344]=5000, [7348]=690, [7349]=861, [7350]=29, + [7351]=58, [7352]=1306, [7353]=2998, [7354]=1864, [7355]=1031, [7356]=1411, + [7357]=1875, [7358]=885, [7359]=978, [7360]=400, [7361]=450, [7362]=500, + [7363]=525, [7364]=550, [7366]=1098, [7367]=1818, [7368]=2677, [7369]=2955, + [7370]=1012, [7371]=500, [7372]=500, [7373]=3097, [7374]=3760, [7375]=3773, + [7377]=2269, [7378]=2146, [7386]=2387, [7387]=2587, [7390]=4237, [7391]=4253, + [7392]=200, [7406]=1372, [7407]=3666, [7408]=2281, [7409]=2289, [7410]=1174, + [7411]=1769, [7412]=1302, [7413]=2157, [7414]=3175, [7415]=1484, [7416]=1442, + [7417]=2903, [7418]=4256, [7419]=1326, [7420]=2658, [7421]=1778, [7422]=1622, + [7423]=3941, [7424]=2709, [7426]=1243, [7427]=3788, [7428]=250, [7429]=4620, + [7430]=4194, [7431]=3899, [7432]=2717, [7433]=1684, [7434]=2536, [7435]=2749, + [7436]=2111, [7437]=1554, [7438]=1560, [7439]=5425, [7440]=5042, [7441]=3514, + [7443]=2243, [7444]=3377, [7445]=3660, [7446]=3092, [7447]=2068, [7448]=2076, + [7449]=625, [7450]=500, [7451]=700, [7452]=875, [7453]=875, [7454]=5915, + [7455]=5937, [7456]=4139, [7457]=2564, [7458]=3983, [7459]=4317, [7460]=2000, + [7461]=2429, [7462]=2682, [7463]=6202, [7465]=6747, [7466]=1921, [7467]=5658, + [7468]=6763, [7469]=6284, [7470]=3668, [7471]=2455, [7472]=3423, [7473]=3710, + [7474]=3281, [7475]=2371, [7476]=2203, [7477]=8124, [7478]=6991, [7479]=4873, + [7480]=3019, [7481]=4908, [7482]=4926, [7483]=4238, [7484]=3062, [7485]=3073, + [7486]=9325, [7487]=8665, [7488]=6038, [7489]=3740, [7490]=5680, [7491]=6158, + [7492]=3015, [7493]=3268, [7494]=3543, [7495]=9558, [7496]=8882, [7497]=1983, + [7506]=500, [7507]=400, [7508]=400, [7509]=553, [7510]=556, [7511]=1938, + [7512]=1945, [7513]=9805, [7514]=9842, [7515]=5382, [7517]=9372, [7518]=9406, + [7519]=7635, [7520]=5747, [7521]=3560, [7522]=4962, [7523]=4993, [7524]=4297, + [7525]=3106, [7526]=3367, [7527]=11284, [7528]=9894, [7529]=6897, [7530]=4273, + [7531]=6434, [7532]=6975, [7533]=5557, [7534]=4016, [7535]=4353, [7536]=13048, + [7537]=14012, [7538]=12322, [7539]=12692, [7540]=8845, [7541]=5479, [7542]=8284, + [7543]=8123, [7544]=4296, [7545]=4658, [7546]=5050, [7547]=3092, [7548]=6507, + [7549]=6420, [7550]=7645, [7551]=7145, [7552]=2542, [7553]=2542, [7554]=1039, + [7555]=7123, [7556]=5387, [7557]=7596, [7558]=1609, [7559]=666, [7560]=300, + [7561]=500, [7606]=557, [7607]=1399, [7608]=1106, [7609]=4210, [7610]=5811, + [7611]=7364, [7612]=7, [7613]=500, [7673]=8990, [7676]=30, [7678]=50, + [7682]=7678, [7683]=7755, [7684]=2184, [7685]=5468, [7686]=4308, [7687]=8836, + [7688]=5320, [7689]=11124, [7690]=2232, [7691]=2689, [7706]=3, [7708]=6649, + [7709]=3559, [7710]=12279, [7711]=1171, [7712]=881, [7713]=4777, [7714]=3835, + [7717]=18923, [7718]=6867, [7719]=6863, [7720]=5356, [7721]=17922, [7722]=9295, + [7723]=22567, [7724]=5436, [7726]=11681, [7727]=2488, [7728]=3223, [7729]=5514, + [7730]=10146, [7731]=3482, [7734]=15495, [7736]=11970, [7738]=211, [7739]=480, + [7742]=600, [7743]=0, [7746]=8430, [7747]=7754, [7748]=10862, [7749]=4885, + [7750]=1712, [7751]=1614, [7752]=6902, [7753]=7874, [7754]=1959, [7755]=4140, + [7756]=1606, [7757]=12874, [7758]=15074, [7759]=6723, [7760]=6074, [7761]=11290, + [7762]=10, [7786]=5605, [7787]=3960, [7788]=10, [7806]=10, [7807]=10, + [7808]=10, [7809]=1024, [7814]=10, [7826]=4, [7849]=10, [7873]=10, + [7888]=8990, [7909]=1000, [7910]=5000, [7911]=500, [7912]=100, [7913]=2500, + [7914]=3330, [7915]=3337, [7916]=3700, [7917]=2711, [7918]=3701, [7919]=2476, + [7920]=8053, [7921]=5387, [7922]=3964, [7924]=4103, [7925]=4447, [7926]=5952, + [7927]=2987, [7928]=4857, [7929]=7739, [7930]=7045, [7931]=7955, [7932]=10365, + [7933]=5769, [7934]=5790, [7935]=8368, [7936]=6739, [7937]=6763, [7938]=4028, + [7939]=10899, [7940]=10, [7941]=12520, [7942]=14659, [7943]=15891, [7944]=20092, + [7945]=17291, [7946]=12576, [7947]=24892, [7948]=1357, [7949]=3990, [7950]=6473, + [7951]=1372, [7952]=3025, [7953]=4907, [7954]=23159, [7955]=241, [7956]=1944, + [7957]=2205, [7958]=2435, [7959]=33857, [7960]=38548, [7961]=25508, [7963]=6488, + [7964]=40, [7965]=40, [7966]=200, [7967]=250, [7969]=250, [7971]=1000, + [7972]=400, [7973]=46, [7974]=50, [7975]=1500, [7976]=2000, [7977]=2000, + [7978]=750, [7979]=750, [7980]=850, [7981]=1100, [7982]=1100, [7983]=2000, + [7984]=2000, [7985]=2000, [7986]=2500, [7987]=2500, [7988]=2500, [7989]=2500, + [7990]=2500, [7991]=2500, [7992]=2000, [7993]=2500, [7994]=2000, [7995]=1500, + [7996]=244, [7997]=81, [8000]=62, [8006]=12472, [8028]=2500, [8029]=2000, + [8030]=2500, [8067]=0, [8068]=0, [8069]=0, [8071]=1534, [8080]=5987, + [8081]=2834, [8082]=3189, [8083]=2261, [8084]=2703, [8085]=5253, [8086]=3730, + [8088]=2667, [8089]=4016, [8090]=2687, [8091]=2697, [8092]=4062, [8093]=5437, + [8094]=5458, [8106]=10438, [8107]=5938, [8108]=3594, [8109]=5011, [8110]=3622, + [8111]=5890, [8112]=8436, [8113]=9694, [8114]=3677, [8115]=5980, [8116]=4632, + [8117]=7161, [8118]=4436, [8119]=12725, [8120]=6207, [8121]=4843, [8122]=7875, + [8123]=11275, [8124]=7932, [8125]=6367, [8126]=15656, [8127]=5937, [8128]=6229, + [8129]=6253, [8130]=9458, [8131]=10115, [8132]=14485, [8133]=10517, [8134]=15993, + [8135]=11234, [8137]=2807, [8138]=7666, [8139]=3053, [8140]=2837, [8141]=4613, + [8142]=4630, [8143]=7227, [8144]=5036, [8146]=500, [8147]=200, [8148]=250, + [8150]=250, [8151]=250, [8152]=500, [8153]=5, [8154]=250, [8156]=2219, + [8157]=5197, [8158]=2236, [8159]=2244, [8160]=3378, [8161]=3390, [8162]=4900, + [8163]=3688, [8165]=500, [8167]=100, [8168]=100, [8169]=500, [8170]=500, + [8171]=500, [8172]=500, [8173]=1000, [8174]=4131, [8175]=5971, [8176]=4495, + [8177]=71, [8178]=306, [8179]=28, [8180]=240, [8181]=79, [8182]=40, + [8183]=2426, [8184]=2947, [8185]=10952, [8186]=2231, [8187]=3477, [8188]=6414, + [8189]=7567, [8190]=37286, [8191]=7780, [8192]=4782, [8193]=8708, [8194]=13877, + [8195]=5263, [8196]=13979, [8197]=7158, [8198]=4013, [8199]=24661, [8200]=7275, + [8201]=5350, [8202]=9022, [8203]=8628, [8204]=4676, [8205]=4346, [8206]=12704, + [8207]=8978, [8208]=10272, [8209]=8375, [8210]=5537, [8211]=8002, [8212]=11585, + [8213]=8150, [8214]=6230, [8215]=9017, [8216]=6323, [8217]=1000, [8218]=1000, + [8223]=10066, [8224]=7655, [8225]=9222, [8226]=5747, [8227]=10, [8244]=10000, + [8245]=12922, [8246]=7917, [8247]=4997, [8248]=7032, [8249]=5336, [8250]=8034, + [8251]=12080, [8252]=13622, [8253]=5107, [8254]=8639, [8255]=6010, [8256]=10261, + [8257]=6053, [8258]=17396, [8259]=9144, [8260]=6938, [8261]=10617, [8262]=14605, + [8263]=10373, [8264]=7857, [8265]=19914, [8266]=7468, [8267]=8927, [8268]=8960, + [8269]=14362, [8270]=14351, [8271]=20358, [8272]=13697, [8273]=3574, [8274]=10251, + [8275]=22028, [8276]=3901, [8277]=3625, [8278]=5477, [8279]=5937, [8280]=8503, + [8281]=5982, [8282]=15695, [8283]=17138, [8284]=10514, [8285]=6637, [8286]=9427, + [8287]=7087, [8288]=11876, [8289]=16688, [8290]=17585, [8291]=6784, [8292]=11476, + [8293]=8542, [8294]=12651, [8295]=7988, [8296]=20861, [8297]=9113, [8298]=8565, + [8299]=14353, [8300]=20172, [8301]=14463, [8302]=11060, [8303]=26989, [8304]=10513, + [8305]=11743, [8306]=11787, [8307]=18717, [8308]=18703, [8309]=26278, [8310]=19419, + [8311]=5844, [8312]=14810, [8313]=30637, [8314]=5344, [8315]=5061, [8316]=8078, + [8317]=8595, [8318]=12926, [8319]=9181, [8320]=22092, [8343]=500, [8345]=7421, + [8346]=5363, [8347]=5979, [8348]=10819, [8349]=14478, [8350]=1130, [8364]=6, + [8365]=4, [8366]=100, [8367]=18455, [8368]=1000, [8384]=875, [8385]=875, + [8386]=1000, [8387]=1000, [8388]=1250, [8389]=1250, [8390]=1250, [8395]=1000, + [8397]=1000, [8398]=1125, [8399]=1250, [8400]=1250, [8401]=1375, [8402]=1375, + [8403]=5537, [8404]=8002, [8405]=6230, [8406]=8150, [8407]=11585, [8408]=9017, + [8409]=1000, [8429]=31, [8430]=46, [8483]=171, [8484]=68, [8485]=1000, + [8486]=1000, [8487]=1000, [8488]=1000, [8489]=1500, [8490]=1500, [8491]=1500, + [8492]=1000, [8494]=1000, [8495]=1000, [8496]=1000, [8497]=500, [8498]=2500, + [8499]=2500, [8500]=1250, [8501]=1250, [8502]=0, [8503]=0, [8504]=0, + [8505]=0, [8506]=0, [8507]=0, [8508]=178, [8523]=0, [8529]=175, + [8543]=13, [8544]=400, [8545]=600, [8546]=50, [8547]=200000, [8563]=0, + [8564]=200, [8588]=0, [8589]=0, [8591]=0, [8592]=0, [8595]=0, + [8624]=250, [8625]=250, [8626]=250, [8629]=0, [8631]=0, [8632]=0, + [8643]=2500, [8644]=1500, [8645]=750, [8646]=250, [8683]=1, [8703]=6492, + [8743]=2, [8744]=50, [8745]=50, [8746]=682, [8747]=1036, [8748]=774, + [8749]=1603, [8750]=1863, [8751]=2618, [8752]=5217, [8753]=5198, [8754]=3937, + [8755]=3728, [8756]=550, [8757]=725, [8758]=1050, [8759]=1325, [8760]=1675, + [8761]=1675, [8762]=2025, [8763]=2025, [8764]=2425, [8765]=2425, [8766]=200, + [8768]=3250, [8769]=3250, [8770]=3250, [8771]=3250, [8772]=3750, [8773]=7750, + [8774]=4500, [8775]=5000, [8776]=8750, [8777]=10000, [8778]=5000, [8779]=5500, + [8780]=8750, [8781]=7750, [8782]=6250, [8783]=8750, [8784]=7000, [8785]=10000, + [8786]=8750, [8787]=1050, [8788]=4500, [8789]=11000, [8790]=11000, [8791]=11000, + [8792]=11000, [8793]=7000, [8794]=12000, [8795]=13250, [8796]=13250, [8797]=13250, + [8798]=13250, [8799]=14750, [8800]=14750, [8801]=14750, [8802]=2, [8803]=20, + [8804]=225, [8805]=325, [8806]=550, [8807]=550, [8808]=725, [8809]=50, + [8810]=725, [8811]=1050, [8812]=1050, [8813]=1325, [8814]=1325, [8815]=1675, + [8816]=1675, [8818]=2025, [8819]=2025, [8820]=2425, [8821]=2425, [8822]=1050, + [8823]=2425, [8824]=2425, [8825]=5500, [8826]=2750, [8827]=125, [8828]=2750, + [8829]=3250, [8830]=2750, [8831]=300, [8832]=3750, [8833]=3750, [8834]=3750, + [8835]=3750, [8836]=95, [8837]=7000, [8838]=60, [8839]=375, [8840]=4500, + [8841]=4500, [8842]=4500, [8843]=5000, [8844]=5000, [8845]=375, [8846]=250, + [8847]=7000, [8848]=5000, [8849]=5000, [8850]=5000, [8851]=5500, [8852]=5500, + [8853]=5500, [8854]=5500, [8855]=5500, [8856]=6250, [8857]=6250, [8858]=6250, + [8859]=7000, [8860]=7750, [8861]=7750, [8862]=7750, [8863]=7750, [8864]=7750, + [8865]=8750, [8866]=8750, [8867]=8750, [8868]=8750, [8869]=8750, [8870]=8750, + [8871]=10000, [8872]=10000, [8873]=10000, [8874]=10000, [8875]=10000, [8876]=2750, + [8877]=11000, [8878]=11000, [8879]=11000, [8880]=11000, [8881]=11000, [8882]=11000, + [8883]=12000, [8884]=12000, [8885]=12000, [8886]=12000, [8887]=13250, [8888]=13250, + [8889]=13250, [8890]=14750, [8891]=14750, [8892]=14750, [8893]=14750, [8894]=14750, + [8895]=14750, [8896]=14750, [8897]=14750, [8898]=4500, [8899]=5000, [8900]=4500, + [8901]=6250, [8902]=5000, [8903]=5000, [8904]=7000, [8905]=5500, [8906]=6250, + [8907]=5500, [8909]=50, [8910]=75, [8911]=75, [8912]=75, [8913]=325, + [8914]=1050, [8915]=1325, [8916]=1675, [8917]=2425, [8918]=2425, [8919]=2750, + [8920]=3250, [8921]=5000, [8922]=5000, [8923]=50, [8924]=25, [8925]=125, + [8926]=75, [8927]=100, [8928]=125, [8929]=6250, [8930]=7000, [8931]=7000, + [8932]=200, [8933]=7000, [8934]=7750, [8935]=8750, [8936]=8750, [8937]=8750, + [8938]=10000, [8939]=10000, [8940]=11000, [8941]=11000, [8942]=12000, [8943]=12000, + [8944]=13250, [8945]=14750, [8947]=14750, [8948]=200, [8949]=200, [8950]=200, + [8951]=200, [8952]=200, [8953]=200, [8954]=2, [8955]=50, [8956]=200, + [8957]=200, [8958]=75, [8959]=160, [8960]=225, [8961]=225, [8962]=325, + [8963]=725, [8964]=725, [8965]=725, [8966]=725, [8967]=1050, [8968]=1325, + [8969]=1675, [8971]=2025, [8972]=2025, [8974]=2425, [8975]=2425, [8976]=2750, + [8977]=2750, [8978]=2750, [8980]=3250, [8981]=3750, [8983]=4500, [8984]=100, + [8985]=150, [8986]=5000, [8987]=5000, [8988]=5000, [8989]=5000, [8990]=5000, + [8991]=5000, [8992]=5500, [8993]=5500, [8994]=5500, [8995]=5500, [8996]=5500, + [8997]=6250, [8998]=6250, [8999]=6250, [9000]=6250, [9001]=7000, [9002]=7000, + [9003]=7000, [9004]=7750, [9005]=7750, [9006]=7750, [9007]=8750, [9008]=8750, + [9009]=8750, [9010]=8750, [9011]=8750, [9012]=8750, [9013]=10000, [9014]=10000, + [9015]=10000, [9016]=11000, [9017]=11000, [9018]=11000, [9019]=12000, [9020]=12000, + [9021]=12000, [9022]=12000, [9023]=12000, [9024]=13250, [9025]=13250, [9026]=13250, + [9027]=13250, [9028]=13250, [9029]=14750, [9030]=200, [9031]=14750, [9032]=14750, + [9033]=14750, [9034]=14750, [9035]=14750, [9036]=20, [9037]=2, [9039]=10, + [9040]=20, [9041]=20, [9043]=50, [9044]=50, [9046]=75, [9047]=75, + [9048]=150, [9049]=150, [9050]=150, [9051]=225, [9052]=225, [9053]=325, + [9054]=550, [9055]=550, [9056]=550, [9057]=725, [9058]=725, [9059]=75, + [9060]=1000, [9061]=250, [9062]=1050, [9063]=1050, [9064]=1050, [9065]=1050, + [9066]=1325, [9067]=1325, [9068]=1325, [9069]=1325, [9070]=1325, [9071]=1675, + [9072]=1675, [9073]=1675, [9074]=2025, [9075]=550, [9076]=550, [9077]=550, + [9078]=2425, [9079]=2425, [9080]=2425, [9081]=2425, [9082]=2425, [9083]=2425, + [9084]=2750, [9085]=2750, [9086]=2750, [9087]=2750, [9088]=250, [9089]=3250, + [9090]=3250, [9091]=3250, [9092]=3250, [9093]=3750, [9094]=3750, [9095]=3750, + [9096]=4500, [9097]=4500, [9098]=4500, [9099]=4500, [9100]=5000, [9101]=5000, + [9102]=5000, [9103]=5000, [9104]=5000, [9105]=5000, [9123]=5500, [9124]=5500, + [9125]=5500, [9126]=5500, [9127]=5500, [9128]=5500, [9129]=6250, [9130]=6250, + [9131]=6250, [9132]=7000, [9133]=7000, [9134]=7000, [9135]=7000, [9136]=7000, + [9137]=7750, [9138]=7750, [9139]=7750, [9140]=7750, [9141]=7750, [9142]=7750, + [9143]=7750, [9144]=250, [9145]=8750, [9146]=8750, [9147]=8750, [9148]=8750, + [9149]=250, [9150]=10000, [9151]=10000, [9152]=10000, [9154]=300, [9155]=400, + [9156]=8750, [9157]=8750, [9158]=8750, [9159]=8750, [9160]=8750, [9161]=8750, + [9162]=12000, [9164]=12000, [9165]=12000, [9166]=12000, [9167]=12000, [9168]=12000, + [9169]=13250, [9170]=13250, [9171]=13250, [9172]=500, [9174]=13250, [9175]=13250, + [9176]=13250, [9177]=14750, [9178]=14750, [9179]=1000, [9180]=14750, [9181]=14750, + [9182]=14750, [9183]=5000, [9184]=5000, [9185]=4500, [9186]=175, [9187]=600, + [9188]=725, [9190]=2, [9191]=10, [9192]=10, [9193]=25, [9194]=50, + [9195]=75, [9197]=600, [9198]=150, [9199]=150, [9200]=225, [9201]=325, + [9202]=550, [9203]=725, [9204]=725, [9205]=725, [9206]=700, [9207]=1325, + [9208]=1325, [9209]=1325, [9210]=750, [9211]=1675, [9212]=2025, [9214]=2500, + [9215]=2025, [9216]=2025, [9217]=2425, [9218]=2425, [9219]=2425, [9221]=2750, + [9222]=2750, [9223]=3250, [9224]=700, [9225]=3750, [9226]=3750, [9227]=4500, + [9228]=4500, [9229]=4500, [9230]=5000, [9231]=5000, [9233]=500, [9242]=2421, + [9243]=8155, [9249]=1553, [9251]=62, [9252]=62, [9253]=62, [9259]=64, + [9260]=400, [9261]=250, [9262]=1000, [9264]=35, [9265]=15, [9276]=100, + [9279]=45, [9285]=2388, [9286]=6040, [9287]=2406, [9288]=2414, [9289]=3635, + [9290]=3940, [9291]=5286, [9292]=3684, [9293]=1250, [9294]=2000, [9295]=2000, + [9296]=2000, [9297]=2500, [9298]=2250, [9300]=2500, [9301]=2500, [9302]=2250, + [9303]=2000, [9304]=2000, [9305]=2000, [9308]=38, [9312]=1, [9313]=0, + [9314]=12, [9315]=1, [9317]=25, [9318]=0, [9327]=625, [9332]=38, + [9333]=73, [9334]=47, [9335]=52, [9336]=1288, [9338]=73, [9355]=376, + [9356]=217, [9357]=227, [9358]=228, [9359]=19577, [9360]=400, [9361]=400, + [9363]=12, [9366]=3689, [9367]=1250, [9372]=61936, [9375]=4220, [9378]=11380, + [9379]=26383, [9380]=28334, [9381]=8628, [9382]=3607, [9383]=16895, [9384]=9789, + [9385]=12280, [9386]=9859, [9387]=3438, [9388]=3196, [9389]=4331, [9390]=2146, + [9391]=12831, [9392]=12980, [9393]=7882, [9394]=1372, [9395]=1565, [9396]=7319, + [9397]=3672, [9398]=1244, [9399]=3, [9400]=8722, [9401]=5070, [9402]=25308, + [9403]=7162, [9404]=2819, [9405]=2055, [9406]=4161, [9407]=5094, [9408]=18639, + [9409]=4490, [9410]=3785, [9411]=8587, [9412]=16681, [9413]=32248, [9414]=10374, + [9415]=8997, [9416]=28218, [9417]=10795, [9418]=32851, [9419]=21692, [9420]=4083, + [9422]=13052, [9423]=25463, [9424]=13594, [9425]=21489, [9426]=10275, [9427]=14853, + [9428]=2096, [9429]=5236, [9430]=8553, [9431]=4885, [9432]=3027, [9433]=4133, + [9434]=5648, [9435]=2916, [9443]=1, [9444]=1120, [9445]=2260, [9446]=7564, + [9447]=3237, [9448]=1154, [9449]=9564, [9450]=2181, [9451]=25, [9452]=8210, + [9453]=6592, [9454]=1984, [9455]=1825, [9456]=6652, [9457]=8902, [9458]=6917, + [9459]=13560, [9461]=4586, [9465]=18560, [9467]=22478, [9469]=14634, [9470]=7858, + [9473]=13241, [9474]=13290, [9475]=33346, [9476]=8593, [9477]=35936, [9478]=28853, + [9479]=10858, [9480]=31725, [9481]=34067, [9482]=29585, [9483]=19139, [9484]=10962, + [9485]=5288, [9486]=5483, [9487]=3632, [9488]=4419, [9489]=3690, [9490]=6121, + [9491]=1308, [9492]=3172, [9496]=11, [9508]=2352, [9509]=2683, [9510]=2945, + [9511]=21060, [9512]=6341, [9513]=305, [9514]=306, [9515]=2011, [9516]=2071, + [9517]=15737, [9518]=1899, [9519]=2288, [9520]=11461, [9521]=14382, [9522]=4938, + [9527]=21613, [9531]=5264, [9533]=7092, [9534]=8847, [9535]=1406, [9536]=1411, + [9538]=6463, [9539]=50, [9540]=150, [9541]=200, [9587]=6250, [9588]=6463, + [9598]=94, [9599]=142, [9600]=93, [9601]=87, [9602]=301, [9603]=302, + [9604]=5632, [9605]=1356, [9607]=1762, [9608]=5028, [9609]=1009, [9622]=5665, + [9623]=4198, [9624]=5267, [9625]=6344, [9626]=16243, [9627]=2542, [9630]=5780, + [9631]=3868, [9632]=3883, [9633]=5847, [9634]=3381, [9635]=4713, [9636]=3154, + [9637]=4749, [9638]=6875, [9639]=24111, [9640]=4840, [9641]=13041, [9642]=2092, + [9643]=15988, [9644]=9885, [9645]=6768, [9646]=6791, [9647]=7962, [9648]=6392, + [9649]=15875, [9650]=21619, [9651]=32193, [9652]=16159, [9653]=6745, [9654]=16928, + [9655]=7092, [9656]=4251, [9657]=5333, [9658]=4086, [9659]=2, [9660]=4227, + [9661]=9051, [9662]=4258, [9663]=9969, [9664]=5002, [9665]=4826, [9666]=7266, + [9678]=17485, [9679]=17550, [9680]=14090, [9681]=50, [9682]=3831, [9683]=38459, + [9684]=30875, [9685]=32277, [9686]=31097, [9687]=1554, [9698]=1504, [9699]=1812, + [9700]=2, [9701]=2, [9702]=2, [9703]=4272, [9704]=3573, [9705]=3586, + [9706]=9212, [9718]=11421, [9719]=6250, [9742]=49, [9743]=67, [9744]=44, + [9745]=54, [9746]=56, [9747]=227, [9748]=274, [9749]=275, [9750]=57, + [9751]=86, [9752]=57, [9753]=248, [9754]=70, [9755]=73, [9756]=294, + [9757]=363, [9758]=72, [9759]=138, [9760]=73, [9761]=58, [9762]=92, + [9763]=336, [9764]=300, [9765]=406, [9766]=361, [9767]=616, [9768]=317, + [9769]=1637, [9770]=416, [9771]=532, [9772]=1328, [9773]=1369, [9774]=1374, + [9775]=315, [9776]=547, [9777]=318, [9778]=940, [9779]=334, [9780]=369, + [9781]=982, [9782]=1113, [9783]=811, [9784]=533, [9785]=268, [9786]=234, + [9787]=358, [9788]=312, [9789]=741, [9790]=794, [9791]=1644, [9792]=823, + [9793]=550, [9794]=829, [9795]=626, [9796]=1038, [9797]=1528, [9798]=1686, + [9799]=635, [9800]=2027, [9801]=708, [9802]=1204, [9803]=729, [9804]=1874, + [9805]=709, [9806]=833, [9807]=1255, [9808]=1848, [9809]=1855, [9810]=1231, + [9811]=725, [9812]=570, [9813]=825, [9814]=733, [9815]=1664, [9816]=1781, + [9817]=1426, [9818]=1682, [9819]=2726, [9820]=1541, [9821]=871, [9822]=1192, + [9823]=965, [9824]=1599, [9825]=2355, [9826]=2600, [9827]=1114, [9828]=1845, + [9829]=1020, [9830]=3489, [9831]=1233, [9832]=1248, [9833]=3031, [9834]=2074, + [9835]=3053, [9836]=3677, [9837]=1287, [9838]=1174, [9839]=1426, [9840]=1432, + [9841]=3163, [9842]=2391, [9843]=3739, [9844]=3709, [9845]=2136, [9846]=1429, + [9847]=1779, [9848]=1584, [9849]=2385, [9850]=2394, [9851]=3524, [9852]=3820, + [9853]=1368, [9854]=4935, [9855]=1895, [9856]=2839, [9857]=1570, [9858]=4882, + [9859]=2871, [9860]=2382, [9861]=1753, [9862]=4260, [9863]=3207, [9864]=3527, + [9865]=1942, [9866]=5605, [9867]=1778, [9868]=2160, [9869]=2225, [9870]=3686, + [9871]=5425, [9872]=4101, [9873]=5828, [9874]=5801, [9875]=2139, [9876]=3146, + [9877]=2708, [9878]=3424, [9879]=1964, [9880]=2130, [9881]=3464, [9882]=6209, + [9883]=5026, [9884]=5448, [9885]=4179, [9886]=2589, [9887]=7071, [9889]=4240, + [9890]=2919, [9891]=2636, [9892]=2857, [9893]=6689, [9894]=4661, [9895]=5221, + [9896]=2913, [9897]=7368, [9898]=2718, [9899]=7920, [9900]=3195, [9901]=3294, + [9902]=5357, [9903]=7742, [9904]=5420, [9905]=8251, [9906]=3043, [9907]=4581, + [9908]=4257, [9909]=2848, [9910]=3087, [9911]=7228, [9912]=5037, [9913]=8490, + [9914]=7557, [9915]=5091, [9916]=3566, [9917]=5519, [9918]=10212, [9919]=5149, + [9920]=3721, [9921]=6050, [9922]=8745, [9923]=6095, [9924]=10276, [9925]=3789, + [9926]=6368, [9927]=3927, [9928]=9930, [9929]=3662, [9930]=4287, [9931]=4303, + [9932]=6995, [9933]=9394, [9934]=6577, [9935]=10097, [9936]=6403, [9937]=4004, + [9938]=6029, [9939]=4317, [9940]=6954, [9941]=6523, [9942]=9341, [9943]=11271, + [9944]=8570, [9945]=4124, [9946]=11396, [9947]=5193, [9948]=8365, [9949]=4855, + [9950]=13418, [9951]=6795, [9952]=5255, [9953]=8467, [9954]=12125, [9955]=8530, + [9956]=5130, [9957]=13753, [9958]=14724, [9959]=4803, [9960]=5623, [9961]=5644, + [9962]=9218, [9963]=9211, [9964]=13190, [9965]=9973, [9966]=6743, [9967]=2900, + [9968]=2695, [9969]=3964, [9970]=5730, [9971]=3995, [9972]=2475, [9973]=3727, + [9974]=16449, [9998]=4815, [9999]=4832, [10001]=5257, [10002]=5276, [10003]=2859, + [10004]=5738, [10007]=5799, [10008]=4365, [10009]=5284, [10010]=5729, [10011]=2875, + [10018]=3275, [10019]=3944, [10020]=6597, [10021]=7946, [10023]=3334, [10024]=5421, + [10025]=6790, [10026]=5459, [10027]=5479, [10028]=5938, [10029]=5391, [10030]=6007, + [10031]=6030, [10032]=6052, [10033]=6075, [10034]=2000, [10035]=1735, [10036]=1741, + [10038]=2620, [10039]=7110, [10040]=1767, [10041]=8593, [10042]=6644, [10043]=2033, + [10044]=6765, [10045]=23, [10046]=32, [10047]=164, [10048]=935, [10049]=2653, + [10050]=2500, [10051]=2500, [10052]=1500, [10053]=4499, [10054]=3000, [10055]=3000, + [10056]=1500, [10057]=12711, [10058]=7083, [10059]=4739, [10060]=6667, [10061]=7661, + [10062]=4758, [10063]=7165, [10064]=10774, [10065]=12153, [10066]=4220, [10067]=5295, + [10068]=9128, [10069]=5335, [10070]=15482, [10071]=8063, [10072]=6177, [10073]=9858, + [10074]=13985, [10075]=9369, [10076]=6571, [10077]=16968, [10078]=18642, [10079]=6370, + [10080]=7319, [10081]=7345, [10082]=10752, [10083]=10745, [10084]=15246, [10085]=11529, + [10086]=8438, [10087]=3393, [10088]=3154, [10089]=4749, [10090]=5148, [10091]=7441, + [10092]=5602, [10093]=18903, [10094]=3311, [10095]=10295, [10096]=6130, [10097]=10991, + [10098]=8736, [10099]=6567, [10100]=11108, [10101]=15756, [10102]=15020, [10103]=5687, + [10104]=15137, [10105]=19942, [10106]=12120, [10107]=7218, [10108]=8203, [10109]=7273, + [10110]=7947, [10111]=13444, [10112]=19071, [10113]=13541, [10118]=23381, [10119]=16676, + [10120]=8797, [10121]=10515, [10122]=9007, [10123]=15239, [10124]=20396, [10125]=14551, + [10126]=8865, [10127]=4350, [10128]=11907, [10129]=4689, [10130]=4706, [10131]=7085, + [10132]=7608, [10133]=10792, [10134]=7664, [10135]=20663, [10136]=8530, [10137]=13484, + [10138]=12274, [10139]=14258, [10140]=9085, [10141]=20103, [10142]=13401, [10143]=19772, + [10144]=8164, [10145]=9755, [10146]=16194, [10147]=10319, [10148]=11273, [10149]=10915, + [10150]=17254, [10151]=25456, [10152]=24333, [10153]=17444, [10154]=13337, [10155]=20168, + [10156]=12794, [10157]=31216, [10158]=31040, [10159]=11444, [10160]=19948, [10161]=12713, + [10162]=28137, [10163]=20264, [10164]=14808, [10165]=6240, [10166]=5908, [10167]=9429, + [10168]=10032, [10169]=14230, [10170]=10105, [10171]=6018, [10172]=9603, [10173]=5718, + [10174]=8121, [10175]=9010, [10176]=5689, [10177]=12833, [10178]=14475, [10179]=8632, + [10180]=5141, [10181]=14640, [10182]=18370, [10183]=10954, [10184]=6523, [10185]=7412, + [10186]=6966, [10187]=11785, [10188]=16717, [10189]=11872, [10190]=6669, [10191]=7777, + [10192]=13212, [10193]=19775, [10194]=7345, [10195]=19221, [10196]=7593, [10197]=7623, + [10198]=12167, [10199]=17261, [10200]=12314, [10201]=5960, [10202]=3988, [10203]=10397, + [10204]=19888, [10205]=4316, [10206]=4048, [10207]=6695, [10208]=9586, [10209]=7216, + [10210]=12845, [10211]=12276, [10212]=11069, [10213]=7850, [10214]=8271, [10215]=17387, + [10216]=7180, [10217]=16688, [10218]=17589, [10219]=12011, [10220]=23263, [10221]=9148, + [10222]=14462, [10223]=9466, [10224]=10755, [10225]=10012, [10226]=16616, [10227]=23348, + [10228]=15940, [10229]=10331, [10230]=25686, [10231]=9815, [10232]=11732, [10233]=11106, + [10234]=17801, [10235]=16896, [10236]=22615, [10237]=16287, [10238]=7650, [10239]=4962, + [10240]=12576, [10241]=8425, [10242]=5318, [10243]=5035, [10244]=12039, [10245]=8548, + [10246]=21542, [10247]=14707, [10248]=9371, [10249]=13435, [10250]=14866, [10251]=9946, + [10252]=20959, [10253]=15024, [10254]=22164, [10255]=8934, [10256]=10675, [10257]=16877, + [10258]=12292, [10259]=11335, [10260]=11945, [10261]=17985, [10262]=25272, [10263]=18116, + [10264]=26729, [10265]=13904, [10266]=32310, [10267]=13337, [10268]=14758, [10269]=14106, + [10270]=22396, [10271]=32592, [10272]=20865, [10273]=29323, [10274]=21118, [10275]=16204, + [10276]=9847, [10277]=6985, [10278]=6239, [10279]=11188, [10280]=15723, [10281]=11272, + [10282]=6712, [10285]=1000, [10286]=400, [10287]=964, [10288]=1410, [10289]=1557, + [10290]=625, [10298]=1243, [10299]=3778, [10300]=1250, [10301]=1250, [10302]=1250, + [10303]=1250, [10304]=1250, [10305]=100, [10306]=100, [10307]=112, [10308]=112, + [10309]=125, [10310]=125, [10311]=750, [10312]=1500, [10313]=1500, [10314]=1000, + [10315]=1750, [10316]=200, [10317]=1000, [10318]=1750, [10319]=1750, [10320]=1750, + [10321]=1125, [10322]=1875, [10323]=1125, [10324]=2000, [10325]=2500, [10326]=1250, + [10328]=6992, [10329]=2507, [10330]=9587, [10331]=2728, [10332]=3790, [10333]=2314, + [10358]=2421, [10359]=2430, [10360]=1250, [10361]=1250, [10362]=24760, [10363]=27396, + [10364]=24938, [10365]=26276, [10366]=32052, [10367]=32971, [10368]=17868, [10369]=7377, + [10370]=6985, [10371]=11149, [10372]=11750, [10373]=17336, [10374]=11837, [10375]=7116, + [10376]=12519, [10377]=7978, [10378]=19466, [10379]=13290, [10380]=8468, [10381]=8094, + [10382]=18808, [10383]=12521, [10384]=20370, [10385]=13247, [10386]=8865, [10387]=8475, + [10388]=14068, [10389]=19770, [10390]=14175, [10391]=8193, [10392]=1250, [10393]=1250, + [10394]=1250, [10398]=1000, [10399]=1467, [10400]=563, [10401]=255, [10402]=385, + [10403]=451, [10404]=864, [10405]=409, [10406]=1976, [10407]=431, [10408]=2390, + [10409]=2409, [10410]=1256, [10411]=787, [10412]=405, [10413]=307, [10418]=16464, + [10421]=32, [10423]=2842, [10424]=750, [10450]=396, [10455]=6464, [10457]=142, + [10460]=629, [10461]=3082, [10462]=3608, [10463]=1750, [10479]=24, [10498]=16, + [10499]=2105, [10500]=3478, [10501]=4398, [10502]=4088, [10503]=5169, [10504]=7770, + [10505]=250, [10506]=5227, [10507]=350, [10508]=8958, [10510]=11369, [10512]=2, + [10513]=5, [10514]=750, [10518]=4696, [10542]=5255, [10543]=3517, [10544]=25, + [10545]=3929, [10546]=1500, [10547]=81, [10548]=2500, [10549]=162, [10550]=48, + [10553]=131, [10554]=119, [10558]=250, [10559]=750, [10560]=1000, [10561]=1000, + [10562]=750, [10567]=8725, [10568]=2, [10570]=15875, [10571]=10927, [10572]=9592, + [10573]=13754, [10574]=3775, [10576]=6000, [10577]=2000, [10578]=3377, [10579]=0, + [10580]=750, [10581]=5310, [10582]=3606, [10583]=6191, [10584]=2905, [10585]=0, + [10586]=750, [10587]=1500, [10588]=5834, [10591]=2, [10592]=150, [10601]=500, + [10602]=750, [10603]=825, [10604]=825, [10605]=875, [10606]=875, [10607]=900, + [10608]=950, [10609]=1000, [10611]=2, [10612]=2, [10613]=2, [10614]=2, + [10615]=2, [10616]=2, [10617]=2, [10618]=2, [10619]=2, [10620]=250, + [10623]=24748, [10624]=17249, [10625]=27366, [10626]=36732, [10627]=26974, [10628]=29245, + [10629]=8066, [10630]=8664, [10631]=6086, [10632]=7624, [10633]=11684, [10634]=10812, + [10635]=14, [10636]=6, [10637]=139, [10638]=210, [10644]=500, [10645]=750, + [10646]=500, [10647]=500, [10648]=125, [10651]=257, [10652]=36370, [10653]=1639, + [10654]=965, [10655]=9, [10656]=14, [10657]=933, [10658]=1249, [10659]=4662, + [10683]=8750, [10684]=500, [10685]=2, [10686]=25395, [10696]=41890, [10697]=42044, + [10698]=52747, [10700]=4983, [10701]=7536, [10702]=5379, [10703]=16792, [10704]=13649, + [10705]=3666, [10706]=5521, [10707]=7772, [10708]=8982, [10709]=10953, [10710]=6130, + [10711]=8377, [10713]=500, [10716]=750, [10719]=1500, [10720]=750, [10721]=3317, + [10723]=750, [10724]=4697, [10725]=1500, [10726]=5520, [10727]=2000, [10728]=375, + [10739]=5292, [10740]=10692, [10741]=10063, [10742]=9589, [10743]=10830, [10744]=24159, + [10745]=8498, [10746]=4549, [10747]=2327, [10748]=4380, [10749]=10554, [10750]=35315, + [10751]=10634, [10756]=2, [10758]=20144, [10760]=2914, [10761]=17193, [10762]=6903, + [10763]=5197, [10764]=10434, [10765]=4114, [10766]=9660, [10767]=9652, [10768]=4541, + [10769]=5277, [10770]=7835, [10771]=2910, [10772]=14604, [10774]=5957, [10775]=6377, + [10776]=4799, [10777]=4014, [10778]=16930, [10779]=8807, [10780]=5542, [10781]=15072, + [10782]=4539, [10783]=11476, [10784]=18432, [10785]=13200, [10786]=11977, [10787]=6382, + [10788]=5338, [10795]=5542, [10796]=8982, [10797]=35069, [10798]=6263, [10799]=39288, + [10800]=7562, [10801]=11704, [10802]=7831, [10803]=29441, [10804]=29551, [10805]=29661, + [10806]=15146, [10807]=15202, [10808]=7629, [10820]=138, [10821]=208, [10822]=2500, + [10823]=17796, [10824]=5930, [10825]=2, [10826]=25501, [10827]=10238, [10828]=40427, + [10829]=10680, [10830]=750, [10833]=8597, [10835]=24645, [10836]=28989, [10837]=38801, + [10838]=34665, [10841]=85, [10842]=17595, [10843]=10595, [10844]=44314, [10845]=14233, + [10846]=16143, [10847]=56921, [10858]=750, [10878]=2, [10898]=2, [10918]=42, + [10919]=214, [10920]=67, [10921]=125, [10922]=175, [10938]=0, [10939]=0, + [10940]=0, [10959]=8750, [10978]=0, [10998]=0, [11018]=146, [11019]=2, + [11020]=0, [11021]=1, [11022]=250, [11023]=2500, [11025]=2, [11026]=2500, + [11027]=2500, [11038]=200, [11039]=200, [11040]=1, [11041]=1, [11042]=2, + [11081]=200, [11082]=0, [11083]=0, [11084]=0, [11086]=27403, [11087]=2, + [11098]=500, [11099]=250, [11101]=625, [11105]=0, [11109]=6, [11110]=2, + [11111]=134, [11118]=10795, [11120]=30144, [11121]=2941, [11122]=7162, [11123]=15037, + [11124]=16980, [11128]=500, [11130]=500, [11134]=0, [11135]=0, [11137]=0, + [11138]=0, [11139]=0, [11141]=0, [11144]=1000, [11145]=1250, [11150]=750, + [11151]=750, [11152]=750, [11163]=750, [11164]=750, [11165]=750, [11166]=1000, + [11167]=1000, [11168]=1000, [11174]=0, [11175]=0, [11176]=0, [11177]=0, + [11178]=0, [11187]=5, [11189]=10, [11190]=5, [11191]=45, [11192]=4, + [11193]=16938, [11194]=20404, [11195]=13655, [11196]=16875, [11199]=2, [11200]=2, + [11201]=2, [11202]=1100, [11203]=1100, [11204]=1100, [11205]=1250, [11206]=1250, + [11207]=3000, [11208]=1350, [11223]=1450, [11224]=1450, [11225]=1550, [11226]=1550, + [11229]=1696, [11262]=8142, [11263]=10103, [11264]=2, [11265]=16484, [11284]=1, + [11285]=2, [11287]=508, [11288]=1535, [11289]=3581, [11290]=5263, [11291]=1125, + [11302]=7130, [11303]=636, [11304]=972, [11305]=5162, [11306]=3893, [11307]=13590, + [11308]=15765, [11310]=6598, [11311]=6132, [11314]=2, [11317]=2, [11321]=2, + [11322]=2, [11323]=2, [11324]=6250, [11325]=150, [11342]=2, [11343]=2, + [11362]=250, [11363]=250, [11365]=2, [11369]=2, [11370]=500, [11371]=600, + [11382]=750, [11383]=2, [11384]=70, [11385]=145, [11386]=676, [11387]=1013, + [11388]=1563, [11389]=2163, [11390]=80, [11391]=205, [11392]=403, [11393]=780, + [11394]=580, [11395]=830, [11402]=1205, [11403]=1592, [11404]=2080, [11406]=168, + [11407]=108, [11408]=898, [11409]=503, [11410]=578, [11411]=1484, [11414]=1828, + [11415]=200, [11416]=328, [11417]=1204, [11418]=604, [11419]=1900, [11420]=1712, + [11424]=2, [11444]=200, [11469]=3563, [11474]=500, [11475]=7, [11502]=6510, + [11505]=2, [11506]=2, [11508]=1, [11513]=1, [11514]=1, [11515]=1, + [11542]=2, [11562]=1000, [11563]=1000, [11564]=1000, [11565]=1000, [11566]=1000, + [11567]=1000, [11584]=1, [11585]=1, [11586]=1, [11587]=1, [11588]=2, + [11589]=1, [11590]=250, [11591]=2, [11603]=30656, [11604]=19428, [11605]=10776, + [11606]=19255, [11607]=51225, [11608]=45760, [11610]=3000, [11611]=3000, [11612]=3000, + [11614]=3000, [11615]=3000, [11623]=9999, [11624]=9079, [11625]=10452, [11626]=9698, + [11627]=14668, [11628]=24433, [11629]=24527, [11630]=5, [11631]=19893, [11632]=9360, + [11633]=14076, [11634]=7063, [11635]=35451, [11662]=7462, [11665]=8532, [11669]=17157, + [11675]=14464, [11677]=11655, [11678]=15596, [11679]=11738, [11684]=63086, [11685]=13941, + [11686]=9329, [11702]=36752, [11703]=7378, [11722]=16610, [11726]=33533, [11728]=25336, + [11729]=19073, [11730]=12762, [11731]=19300, [11735]=16669, [11742]=8750, [11743]=30734, + [11744]=39242, [11745]=7878, [11746]=11861, [11747]=16659, [11748]=25083, [11749]=20141, + [11750]=43234, [11755]=14627, [11762]=2, [11763]=2, [11764]=10398, [11765]=12525, + [11766]=8606, [11767]=8637, [11768]=7224, [11782]=12720, [11783]=12767, [11784]=33835, + [11785]=29083, [11786]=50739, [11787]=13732, [11802]=17976, [11803]=50177, [11805]=40439, + [11807]=9153, [11808]=19292, [11809]=51287, [11810]=10000, [11811]=1500, [11812]=13979, + [11813]=3000, [11814]=13083, [11815]=10000, [11816]=48882, [11817]=39254, [11819]=10000, + [11820]=26761, [11821]=22384, [11822]=14153, [11823]=23677, [11824]=13657, [11825]=2500, + [11826]=2500, [11827]=675, [11828]=675, [11832]=10000, [11838]=2, [11839]=11191, + [11840]=7137, [11841]=15032, [11842]=17050, [11845]=62, [11846]=30, [11847]=7, + [11848]=5, [11849]=7, [11850]=7, [11851]=12, [11852]=14, [11853]=388, + [11854]=1494, [11855]=782, [11856]=15775, [11857]=21377, [11858]=4768, [11859]=7135, + [11860]=12970, [11861]=5207, [11862]=7042, [11863]=27191, [11864]=34108, [11865]=8708, + [11866]=9819, [11867]=8769, [11868]=6542, [11869]=6542, [11870]=10287, [11871]=10666, + [11872]=8566, [11873]=9661, [11874]=12124, [11875]=5776, [11876]=16287, [11882]=20520, + [11884]=1846, [11885]=711, [11887]=12, [11888]=4879, [11889]=7345, [11902]=34194, + [11903]=1000, [11904]=13815, [11905]=6203, [11906]=32723, [11907]=41050, [11908]=9886, + [11909]=8267, [11910]=12345, [11911]=12392, [11913]=14044, [11915]=20124, [11916]=11834, + [11917]=6334, [11918]=9537, [11919]=9572, [11920]=40732, [11921]=54167, [11922]=43491, + [11923]=43648, [11924]=20475, [11925]=16484, [11926]=24590, [11927]=16456, [11928]=22045, + [11929]=16581, [11930]=14588, [11931]=58103, [11932]=58322, [11933]=19646, [11934]=19921, + [11935]=13162, [11936]=284, [11937]=187, [11938]=213, [11939]=671, [11940]=389, + [11941]=5896, [11942]=5303, [11943]=21485, [11944]=8821, [11945]=6592, [11946]=7912, + [11962]=7939, [11963]=10025, [11964]=30156, [11965]=464, [11966]=164, [11967]=1087, + [11968]=997, [11969]=1721, [11970]=1710, [11971]=3969, [11972]=4649, [11973]=3971, + [11974]=4971, [11975]=4739, [11976]=7778, [11977]=7896, [11978]=7414, [11979]=7471, + [11980]=10539, [11981]=496, [11982]=1062, [11983]=1130, [11984]=2189, [11985]=2144, + [11986]=1745, [11987]=2885, [11988]=7113, [11989]=7471, [11990]=8306, [11991]=6317, + [11992]=7396, [11993]=874, [11994]=1312, [11995]=914, [11996]=1713, [11997]=6469, + [11998]=2896, [11999]=5538, [12000]=41053, [12001]=4971, [12002]=6322, [12003]=250, + [12004]=9163, [12005]=8813, [12006]=1064, [12007]=1064, [12008]=895, [12009]=2174, + [12010]=2469, [12011]=4649, [12012]=2463, [12013]=4649, [12014]=6289, [12015]=8811, + [12016]=7781, [12017]=8963, [12018]=13842, [12019]=4220, [12020]=3969, [12021]=9327, + [12022]=4718, [12023]=4971, [12024]=5396, [12025]=5282, [12026]=7757, [12027]=6257, + [12028]=4007, [12029]=5395, [12030]=7143, [12031]=7894, [12032]=5396, [12034]=5012, + [12035]=5513, [12036]=5982, [12037]=87, [12038]=9092, [12039]=4224, [12040]=4164, + [12041]=14446, [12042]=4749, [12043]=5396, [12044]=5145, [12045]=7767, [12046]=5757, + [12047]=4996, [12048]=6507, [12049]=16850, [12050]=8963, [12051]=8996, [12052]=837, + [12053]=837, [12054]=837, [12055]=8375, [12056]=8375, [12057]=8375, [12058]=8376, + [12059]=12377, [12061]=40212, [12062]=40365, [12063]=2, [12064]=1, [12065]=7953, + [12066]=11149, [12082]=13726, [12083]=7348, [12102]=9042, [12103]=12157, [12104]=20879, + [12105]=25144, [12106]=16824, [12107]=16885, [12108]=23054, [12109]=11569, [12110]=11610, + [12111]=7768, [12112]=10406, [12113]=10443, [12114]=8734, [12115]=7012, [12142]=2, + [12162]=750, [12163]=1100, [12164]=1100, [12182]=2, [12183]=2, [12184]=87, + [12185]=12895, [12190]=250, [12202]=87, [12203]=87, [12204]=112, [12205]=112, + [12206]=112, [12207]=150, [12208]=150, [12209]=95, [12210]=300, [12211]=300, + [12212]=300, [12213]=300, [12214]=300, [12215]=300, [12216]=300, [12217]=300, + [12218]=300, [12223]=4, [12224]=10, [12225]=187, [12226]=6, [12227]=400, + [12228]=1250, [12229]=1250, [12231]=750, [12232]=1250, [12233]=750, [12238]=2, + [12239]=1750, [12240]=1750, [12243]=45980, [12247]=5674, [12248]=6405, [12249]=6039, + [12250]=8068, [12251]=10779, [12252]=12619, [12253]=3545, [12254]=4483, [12255]=6997, + [12256]=8765, [12257]=3885, [12258]=4549, [12259]=8072, [12260]=10395, [12261]=950, + [12264]=1500, [12282]=8, [12285]=2, [12290]=2, [12294]=2, [12295]=282, + [12296]=722, [12297]=2, [12298]=2, [12299]=16, [12304]=2, [12322]=2, + [12328]=2, [12329]=2, [12331]=2, [12332]=2, [12338]=3, [12348]=2, + [12351]=0, [12353]=0, [12354]=0, [12359]=600, [12360]=5000, [12361]=7000, + [12363]=2000, [12364]=10000, [12365]=250, [12403]=2, [12404]=75, [12405]=9239, + [12406]=4636, [12408]=4998, [12409]=10336, [12410]=10372, [12414]=17376, [12415]=17003, + [12416]=7595, [12417]=17034, [12418]=10242, [12419]=16421, [12420]=25330, [12421]=2, + [12422]=16204, [12424]=5533, [12425]=6044, [12426]=12062, [12427]=12106, [12428]=8646, + [12429]=17923, [12442]=11463, [12443]=17896, [12446]=19, [12447]=19, [12448]=19, + [12449]=19, [12450]=1500, [12451]=1500, [12452]=1, [12453]=1, [12454]=1, + [12455]=1500, [12456]=1, [12457]=1500, [12458]=1500, [12459]=1500, [12460]=1500, + [12461]=2, [12462]=20274, [12463]=42446, [12464]=8520, [12465]=10261, [12466]=6865, + [12468]=8644, [12469]=29179, [12470]=8784, [12471]=8409, [12482]=2, [12502]=2, + [12522]=182, [12523]=1, [12527]=35332, [12528]=37833, [12529]=1500, [12531]=28883, + [12532]=48501, [12535]=34925, [12542]=12356, [12543]=7156, [12544]=7407, [12545]=7082, + [12546]=7463, [12547]=10519, [12548]=7102, [12549]=13669, [12550]=6464, [12551]=11594, + [12552]=10980, [12553]=17699, [12554]=9474, [12555]=11406, [12556]=14317, [12557]=14368, + [12582]=51199, [12583]=74346, [12584]=49636, [12585]=10000, [12587]=21031, [12588]=25446, + [12589]=10251, [12590]=75624, [12591]=2, [12592]=95241, [12593]=2, [12602]=35212, + [12603]=25048, [12604]=14368, [12605]=29106, [12606]=12668, [12607]=8048, [12608]=13232, + [12609]=20498, [12610]=12247, [12611]=12293, [12612]=12956, [12613]=18205, [12614]=18272, + [12615]=27510, [12616]=21841, [12617]=21919, [12618]=23972, [12619]=24058, [12620]=17245, + [12621]=45085, [12624]=20642, [12625]=13080, [12626]=9191, [12628]=16640, [12629]=2, + [12631]=8919, [12632]=14099, [12633]=14859, [12634]=15310, [12636]=24285, [12637]=10316, + [12639]=15271, [12640]=21894, [12641]=43836, [12643]=75, [12644]=200, [12645]=500, + [12651]=36055, [12653]=36318, [12654]=62, [12655]=500, [12662]=600, [12682]=3000, + [12683]=3000, [12684]=3000, [12685]=3000, [12686]=57991, [12687]=3000, [12688]=3000, + [12689]=3500, [12690]=3000, [12691]=4000, [12692]=4000, [12693]=5000, [12694]=5000, + [12695]=5000, [12696]=5000, [12697]=5500, [12698]=5500, [12699]=5000, [12700]=6250, + [12701]=6250, [12702]=6250, [12703]=10000, [12704]=7500, [12705]=7500, [12706]=7500, + [12707]=7500, [12709]=57991, [12711]=10000, [12713]=10000, [12714]=10000, [12715]=10000, + [12716]=15000, [12717]=15000, [12718]=15000, [12719]=15000, [12720]=20000, [12725]=15000, + [12726]=15000, [12727]=15000, [12728]=20000, [12742]=2, [12743]=2, [12744]=2, + [12745]=2, [12746]=2, [12747]=2, [12748]=2, [12749]=2, [12750]=2, + [12751]=2, [12752]=22907, [12754]=2, [12755]=4, [12756]=35057, [12757]=35192, + [12763]=200, [12764]=33621, [12769]=46154, [12772]=38871, [12773]=33079, [12774]=36044, + [12775]=39949, [12776]=48124, [12777]=38648, [12779]=35338, [12781]=42721, [12782]=56808, + [12783]=58215, [12784]=73036, [12786]=2, [12787]=2, [12788]=2, [12790]=74619, + [12791]=39411, [12792]=39255, [12793]=19846, [12794]=56304, [12795]=48821, [12796]=70912, + [12797]=56943, [12798]=57150, [12799]=7000, [12800]=10000, [12801]=2, [12802]=62625, + [12803]=500, [12804]=2000, [12805]=3987, [12808]=1000, [12809]=10000, [12810]=500, + [12811]=20000, [12816]=3000, [12817]=4000, [12818]=3125, [12819]=4000, [12821]=4000, + [12823]=5000, [12824]=5000, [12825]=5000, [12826]=5000, [12827]=5000, [12828]=5500, + [12830]=5500, [12831]=10000, [12832]=10000, [12833]=20000, [12834]=20000, [12835]=20000, + [12836]=20000, [12837]=20000, [12838]=20000, [12839]=20000, [12850]=2, [12851]=2, + [12852]=2, [12853]=2, [12854]=2, [12855]=2, [12856]=2, [12857]=2, + [12858]=2, [12859]=2, [12860]=6000, [12861]=2, [12863]=2, [12864]=2, + [12865]=2, [12866]=2, [12867]=2, [12868]=2, [12869]=2, [12871]=8048, + [12882]=3, [12883]=2, [12889]=2, [12890]=2, [12892]=2, [12893]=1, + [12895]=29461, [12901]=3, [12902]=2, [12903]=45482, [12904]=13584, [12905]=16359, + [12926]=14907, [12927]=19149, [12929]=19646, [12930]=10000, [12931]=1, [12932]=1, + [12933]=1, [12934]=2, [12935]=23196, [12936]=11640, [12937]=2, [12939]=60363, + [12940]=54812, [12941]=1, [12943]=2, [12944]=2, [12945]=42565, [12949]=2, + [12950]=2, [12951]=2, [12952]=14861, [12953]=22374, [12958]=12500, [12959]=3, + [12960]=17752, [12963]=27717, [12964]=33388, [12965]=22344, [12966]=14018, [12967]=16884, + [12968]=16948, [12969]=70884, [12970]=22768, [12974]=6105, [12975]=2205, [12976]=1770, + [12977]=355, [12978]=534, [12979]=617, [12980]=1, [12981]=1, [12982]=850, + [12983]=2362, [12984]=1423, [12985]=1153, [12986]=2, [12987]=906, [12988]=1137, + [12989]=2854, [12990]=2589, [12991]=2, [12992]=3260, [12993]=2, [12994]=787, + [12995]=1, [12996]=1527, [12997]=1920, [12998]=1020, [12999]=853, [13000]=65225, + [13001]=10648, [13002]=14617, [13003]=49640, [13004]=33590, [13005]=1331, [13006]=54840, + [13007]=13585, [13008]=12815, [13009]=15167, [13010]=2241, [13011]=1031, [13012]=1241, + [13013]=12355, [13014]=34705, [13015]=53957, [13016]=4771, [13017]=17209, [13018]=31967, + [13019]=4765, [13020]=8985, [13021]=16694, [13022]=27990, [13023]=42703, [13024]=4863, + [13025]=10464, [13026]=19441, [13027]=33832, [13028]=52605, [13029]=5613, [13030]=13113, + [13031]=1401, [13032]=4138, [13033]=8094, [13034]=14710, [13035]=25141, [13036]=40988, + [13037]=4729, [13038]=9630, [13039]=17892, [13040]=29444, [13041]=4516, [13042]=17596, + [13043]=32389, [13044]=52800, [13045]=10809, [13046]=35042, [13047]=56588, [13048]=5971, + [13049]=6191, [13050]=2, [13051]=22911, [13052]=41677, [13053]=59186, [13054]=15805, + [13055]=29367, [13056]=48330, [13057]=5382, [13058]=21833, [13059]=38719, [13060]=60784, + [13061]=2, [13062]=2991, [13063]=5851, [13064]=11487, [13065]=20945, [13066]=8997, + [13067]=14341, [13068]=4785, [13069]=2, [13070]=14087, [13071]=4061, [13072]=10426, + [13073]=9080, [13074]=8024, [13075]=22090, [13076]=4715, [13077]=8226, [13078]=2, + [13079]=3359, [13080]=22512, [13081]=7123, [13082]=13235, [13083]=34694, [13084]=6614, + [13085]=9137, [13086]=0, [13087]=5896, [13088]=7413, [13089]=8039, [13090]=35955, + [13091]=10637, [13093]=3381, [13094]=2646, [13095]=6646, [13096]=7913, [13097]=2164, + [13098]=15282, [13099]=1431, [13100]=5421, [13101]=14318, [13102]=6879, [13103]=3453, + [13104]=2, [13105]=2147, [13106]=1216, [13107]=11291, [13108]=2446, [13109]=8152, + [13110]=4968, [13111]=13993, [13112]=9624, [13113]=20546, [13114]=2574, [13115]=6990, + [13116]=19370, [13117]=3830, [13118]=10601, [13119]=3063, [13120]=9505, [13121]=3428, + [13122]=11489, [13123]=33387, [13124]=3996, [13125]=12954, [13126]=10374, [13127]=3021, + [13128]=10543, [13129]=7621, [13130]=25065, [13131]=2312, [13132]=7340, [13133]=23083, + [13134]=7716, [13135]=14001, [13136]=1456, [13137]=5874, [13138]=11532, [13139]=21029, + [13141]=12093, [13142]=11642, [13143]=21372, [13144]=6865, [13145]=2704, [13146]=34704, + [13147]=1, [13148]=62629, [13150]=2, [13160]=2, [13161]=65749, [13162]=10558, + [13163]=69537, [13164]=10539, [13165]=2, [13166]=14204, [13167]=59410, [13168]=19085, + [13169]=23948, [13170]=19230, [13171]=7000, [13173]=600, [13175]=30619, [13177]=16396, + [13178]=13782, [13179]=14916, [13181]=8797, [13182]=44142, [13183]=51771, [13184]=13638, + [13185]=16422, [13198]=50851, [13199]=4398, [13203]=15529, [13204]=51945, [13205]=35031, + [13206]=19880, [13208]=12520, [13210]=18024, [13211]=14474, [13212]=10670, [13213]=9633, + [13216]=13250, [13217]=8814, [13218]=53388, [13220]=2, [13222]=2, [13243]=36515, + [13244]=14482, [13245]=1064, [13246]=53622, [13247]=5837, [13248]=29670, [13249]=67782, + [13252]=12434, [13253]=9983, [13254]=25883, [13255]=14366, [13257]=18088, [13258]=13343, + [13259]=16069, [13260]=22561, [13261]=10452, [13262]=261407, [13282]=13684, [13283]=14907, + [13284]=19785, [13285]=54924, [13286]=44106, [13287]=625, [13288]=625, [13290]=1, + [13291]=1, [13292]=1, [13293]=1, [13308]=450, [13309]=250, [13310]=500, + [13311]=2500, [13312]=2, [13314]=30222, [13315]=11396, [13316]=2, [13318]=1, + [13319]=1, [13320]=0, [13321]=0, [13322]=0, [13323]=0, [13324]=0, + [13326]=0, [13327]=0, [13329]=0, [13331]=0, [13332]=0, [13333]=0, + [13334]=0, [13335]=250000, [13336]=2, [13337]=2, [13338]=2, [13339]=2, + [13340]=16498, [13342]=0, [13343]=0, [13344]=17210, [13345]=15457, [13346]=23118, + [13348]=72770, [13349]=58429, [13353]=10452, [13358]=21036, [13359]=25336, [13360]=56517, + [13361]=56730, [13362]=2000, [13363]=2000, [13364]=2000, [13365]=2000, [13366]=3000, + [13368]=55438, [13369]=17526, [13371]=6657, [13372]=63769, [13373]=14846, [13374]=14315, + [13375]=30656, [13376]=13738, [13377]=7, [13378]=21972, [13379]=10734, [13380]=38443, + [13381]=15433, [13382]=10850, [13383]=26864, [13384]=8988, [13385]=13237, [13386]=15722, + [13387]=15779, [13388]=21117, [13389]=21746, [13390]=16367, [13391]=16425, [13392]=12211, + [13393]=62380, [13394]=18176, [13395]=11403, [13396]=30853, [13397]=15201, [13398]=20028, + [13399]=46312, [13400]=9296, [13401]=51440, [13402]=21168, [13403]=9401, [13404]=15895, + [13405]=13102, [13408]=44142, [13409]=8119, [13422]=10, [13423]=125, [13442]=500, + [13443]=400, [13444]=1500, [13445]=500, [13446]=1000, [13447]=1250, [13452]=1250, + [13453]=1250, [13454]=750, [13455]=750, [13456]=750, [13457]=750, [13458]=750, + [13459]=100, [13460]=750, [13461]=750, [13462]=750, [13463]=100, [13464]=100, + [13465]=150, [13466]=250, [13467]=250, [13468]=1000, [13473]=7164, [13474]=24724, + [13475]=8109, [13476]=3000, [13477]=3000, [13478]=3250, [13479]=3500, [13480]=3750, + [13481]=3750, [13482]=0, [13483]=0, [13484]=0, [13485]=0, [13486]=3750, + [13487]=3750, [13488]=3750, [13489]=3750, [13490]=4000, [13491]=4000, [13492]=5000, + [13493]=5000, [13494]=6000, [13495]=6000, [13496]=6000, [13497]=6000, [13498]=19890, + [13499]=6000, [13500]=6000, [13501]=7500, [13502]=11987, [13503]=25000, [13504]=2, + [13505]=91345, [13506]=5000, [13508]=4778, [13509]=5393, [13510]=5000, [13511]=5000, + [13512]=5000, [13513]=5000, [13514]=5820, [13515]=9600, [13517]=0, [13518]=10000, + [13519]=10000, [13520]=10000, [13521]=10000, [13522]=10000, [13524]=12458, [13525]=8698, + [13526]=10897, [13527]=13149, [13528]=13198, [13529]=32303, [13530]=12453, [13531]=21191, + [13532]=12762, [13533]=12810, [13534]=37727, [13535]=16829, [13537]=9884, [13538]=19223, + [13539]=8535, [13546]=62, [13607]=2, [13608]=2, [13610]=0, [13612]=2, + [13622]=2, [13623]=2, [13625]=2, [13627]=2, [13628]=1, [13629]=1, + [13630]=1, [13631]=2, [13632]=2, [13698]=2, [13705]=2, [13706]=2, + [13707]=2, [13708]=2, [13709]=2, [13718]=2, [13719]=2, [13720]=2, + [13721]=2, [13722]=2, [13723]=2, [13724]=300, [13750]=3, [13751]=2, + [13753]=2, [13754]=6, [13755]=7, [13756]=9, [13757]=10, [13758]=4, + [13759]=10, [13760]=10, [13810]=300, [13811]=8988, [13812]=8303, [13813]=300, + [13814]=1, [13816]=10561, [13817]=18796, [13818]=13431, [13819]=19880, [13820]=12042, + [13821]=17993, [13822]=11442, [13823]=11924, [13824]=9015, [13825]=10168, [13842]=375, + [13843]=375, [13844]=375, [13845]=375, [13846]=375, [13847]=375, [13848]=375, + [13849]=375, [13851]=312, [13856]=5112, [13857]=10878, [13858]=10917, [13860]=8741, + [13861]=2, [13863]=6617, [13864]=9553, [13865]=13555, [13866]=11358, [13867]=12570, + [13868]=9665, [13869]=9702, [13870]=5471, [13871]=13436, [13874]=1, [13875]=1, + [13876]=25, [13877]=30, [13878]=32, [13879]=35, [13880]=37, [13881]=100, + [13882]=60, [13883]=60, [13884]=75, [13885]=50, [13886]=50, [13887]=75, + [13888]=12, [13889]=5, [13890]=70, [13891]=100, [13893]=15, [13894]=2, + [13895]=101324, [13896]=11020, [13897]=595, [13898]=57739, [13899]=3528, [13900]=27442, + [13901]=25, [13902]=25, [13903]=25, [13904]=25, [13905]=25, [13906]=50, + [13907]=50, [13908]=55, [13909]=55, [13910]=62, [13911]=80, [13912]=90, + [13913]=100, [13914]=125, [13915]=125, [13916]=150, [13917]=200, [13918]=1, + [13922]=1, [13923]=1, [13924]=1, [13925]=2, [13926]=10000, [13927]=8, + [13928]=8, [13929]=10, [13930]=5, [13931]=12, [13932]=12, [13933]=14, + [13934]=18, [13935]=10, [13937]=87013, [13938]=39304, [13939]=4000, [13940]=4000, + [13941]=4000, [13942]=4000, [13943]=4000, [13944]=26808, [13945]=5000, [13946]=5000, + [13947]=5000, [13948]=5000, [13949]=5000, [13950]=16447, [13951]=11005, [13952]=56683, + [13953]=56880, [13954]=20386, [13955]=16367, [13956]=10950, [13957]=12427, [13958]=9505, + [13959]=9542, [13960]=12828, [13961]=18028, [13962]=12064, [13963]=14533, [13964]=53612, + [13965]=16250, [13966]=16250, [13967]=23338, [13968]=16250, [13969]=16019, [13982]=69826, + [13983]=66754, [13984]=57806, [13986]=17469, [14002]=36720, [14022]=8991, [14023]=8991, + [14024]=52422, [14025]=188, [14042]=10561, [14043]=5955, [14044]=9503, [14045]=13480, + [14046]=5000, [14047]=400, [14048]=2000, [14082]=2, [14083]=20, [14084]=2, + [14085]=2, [14086]=45, [14087]=16, [14088]=24, [14089]=30, [14090]=119, + [14091]=119, [14092]=2, [14093]=21, [14094]=121, [14095]=36, [14096]=325, + [14097]=236, [14098]=43, [14099]=47, [14100]=12089, [14101]=6066, [14102]=60, + [14103]=9716, [14104]=15484, [14105]=1, [14106]=18053, [14107]=13140, [14108]=11112, + [14109]=341, [14110]=84, [14111]=10775, [14112]=13509, [14113]=139, [14114]=242, + [14115]=70, [14116]=106, [14117]=163, [14118]=1, [14119]=378, [14120]=577, + [14121]=579, [14122]=219, [14123]=287, [14124]=292, [14125]=676, [14126]=396, + [14127]=999, [14128]=16093, [14129]=415, [14130]=12768, [14131]=242, [14132]=12441, + [14133]=950, [14134]=11280, [14136]=17025, [14137]=18113, [14138]=20042, [14139]=15840, + [14140]=16693, [14141]=12859, [14142]=6087, [14143]=5763, [14144]=14374, [14145]=1032, + [14146]=14084, [14147]=311, [14148]=208, [14149]=307, [14150]=420, [14151]=1055, + [14152]=28815, [14153]=28920, [14154]=29028, [14155]=20000, [14156]=40000, [14157]=433, + [14158]=1236, [14159]=570, [14160]=251, [14161]=337, [14162]=445, [14163]=1168, + [14164]=312, [14165]=1041, [14166]=238, [14167]=358, [14168]=275, [14169]=286, + [14170]=331, [14171]=738, [14172]=837, [14173]=281, [14174]=368, [14175]=847, + [14176]=920, [14177]=631, [14178]=1532, [14179]=747, [14180]=1870, [14181]=775, + [14182]=1167, [14183]=1710, [14184]=1716, [14185]=647, [14186]=1179, [14187]=717, + [14188]=982, [14189]=1588, [14190]=1932, [14191]=881, [14192]=1946, [14193]=2006, + [14194]=756, [14195]=1252, [14196]=1673, [14197]=925, [14198]=1531, [14199]=1240, + [14200]=2258, [14201]=2060, [14202]=3336, [14203]=2754, [14204]=3041, [14205]=1042, + [14206]=1046, [14207]=3076, [14208]=2316, [14209]=1196, [14210]=1636, [14211]=1325, + [14212]=2194, [14213]=3553, [14214]=2009, [14215]=3579, [14216]=4887, [14217]=1638, + [14218]=2713, [14219]=2046, [14220]=3442, [14221]=1510, [14222]=1834, [14223]=2698, + [14224]=3901, [14225]=4694, [14226]=1731, [14227]=2500, [14228]=3559, [14229]=2387, + [14230]=5164, [14231]=1904, [14232]=3097, [14233]=4476, [14234]=5240, [14235]=1789, + [14236]=2909, [14237]=7206, [14238]=3691, [14239]=3176, [14240]=2294, [14241]=2311, + [14242]=5413, [14243]=4075, [14244]=6870, [14245]=2346, [14246]=4451, [14247]=4467, + [14248]=2372, [14249]=7559, [14250]=3872, [14251]=3332, [14252]=4914, [14253]=2819, + [14254]=7698, [14255]=2628, [14256]=2000, [14257]=6194, [14258]=3109, [14259]=5056, + [14260]=2900, [14261]=4044, [14262]=3156, [14263]=5987, [14264]=8013, [14265]=9853, + [14266]=5606, [14267]=9926, [14268]=3486, [14269]=5669, [14270]=4877, [14271]=6599, + [14272]=3820, [14273]=6374, [14274]=10448, [14275]=11115, [14276]=3598, [14277]=10131, + [14278]=6725, [14279]=4206, [14280]=5919, [14281]=8178, [14282]=4552, [14283]=11646, + [14284]=13135, [14285]=6905, [14286]=4620, [14287]=13282, [14288]=14132, [14289]=5131, + [14290]=7725, [14291]=4830, [14292]=6795, [14293]=9857, [14294]=5223, [14295]=13236, + [14296]=7568, [14297]=13556, [14298]=9628, [14299]=8602, [14300]=7685, [14301]=5451, + [14302]=6517, [14303]=16209, [14304]=6194, [14305]=14349, [14306]=16830, [14307]=12064, + [14308]=17799, [14309]=6866, [14310]=11615, [14311]=6524, [14312]=13541, [14313]=9298, + [14314]=7854, [14315]=16550, [14316]=11271, [14317]=16633, [14318]=18408, [14319]=11401, + [14320]=6855, [14321]=9438, [14322]=13712, [14323]=8322, [14324]=16706, [14325]=13204, + [14326]=19479, [14327]=7659, [14328]=21633, [14329]=14066, [14330]=8962, [14331]=11654, + [14332]=15674, [14333]=9987, [14334]=20044, [14335]=15086, [14336]=20137, [14337]=8546, + [14340]=21778, [14341]=1250, [14342]=4000, [14343]=0, [14344]=0, [14364]=324, + [14365]=214, [14366]=189, [14367]=251, [14368]=346, [14369]=809, [14370]=472, + [14371]=815, [14372]=1430, [14373]=524, [14374]=699, [14375]=528, [14376]=551, + [14377]=602, [14378]=997, [14379]=1615, [14380]=1473, [14382]=1, [14383]=1, + [14384]=1, [14385]=1, [14386]=1, [14387]=1, [14388]=1, [14389]=2, + [14390]=1, [14391]=3, [14392]=4, [14393]=2, [14394]=2, [14397]=1600, + [14398]=2355, [14399]=1465, [14400]=1215, [14401]=1786, [14402]=837, [14403]=1017, + [14404]=2471, [14405]=2480, [14406]=935, [14407]=4266, [14408]=2275, [14409]=1707, + [14410]=2709, [14411]=1387, [14412]=2298, [14413]=3947, [14414]=1276, [14415]=3410, + [14416]=1285, [14417]=5888, [14418]=3098, [14419]=1919, [14420]=2889, [14421]=3945, + [14422]=2262, [14423]=3406, [14424]=5316, [14425]=6223, [14426]=2125, [14427]=9124, + [14428]=4717, [14429]=2644, [14430]=3686, [14431]=2877, [14432]=4679, [14433]=6764, + [14434]=8706, [14435]=2778, [14436]=5270, [14437]=12002, [14438]=6192, [14439]=3836, + [14440]=5347, [14441]=7166, [14442]=4481, [14443]=6745, [14444]=10955, [14445]=12353, + [14446]=4248, [14447]=8801, [14448]=5555, [14449]=10125, [14450]=7364, [14451]=5536, + [14452]=8836, [14453]=15677, [14454]=5598, [14455]=15792, [14456]=20230, [14457]=7575, + [14458]=12573, [14459]=10798, [14460]=14661, [14461]=9342, [14462]=18751, [14463]=14114, + [14464]=20822, [14465]=8596, [14466]=3000, [14467]=3000, [14468]=3000, [14469]=3000, + [14470]=3000, [14471]=3000, [14472]=3000, [14473]=3000, [14474]=3000, [14475]=2, + [14476]=3500, [14477]=3500, [14478]=3500, [14479]=3500, [14480]=4000, [14481]=4000, + [14482]=4000, [14483]=4000, [14484]=4000, [14485]=4000, [14486]=10000, [14487]=53222, + [14488]=5000, [14489]=5000, [14490]=5000, [14491]=5000, [14492]=5000, [14493]=5000, + [14494]=5500, [14495]=5500, [14496]=5500, [14497]=5500, [14498]=6250, [14499]=7500, + [14500]=7500, [14501]=7500, [14502]=13051, [14503]=19652, [14504]=10000, [14505]=10000, + [14506]=10000, [14507]=10000, [14508]=15000, [14509]=15000, [14510]=15000, [14511]=15000, + [14512]=15000, [14513]=15000, [14514]=15000, [14522]=31318, [14524]=2, [14525]=10561, + [14526]=5000, [14527]=2, [14528]=34181, [14529]=500, [14530]=1000, [14531]=59891, + [14532]=2, [14533]=2, [14534]=2, [14535]=3, [14536]=32271, [14537]=17001, + [14538]=17062, [14539]=21404, [14541]=68426, [14543]=10478, [14545]=26397, [14548]=24792, + [14549]=7807, [14550]=8802, [14551]=10601, [14552]=15378, [14553]=17733, [14554]=29900, + [14555]=78772, [14557]=17880, [14558]=10500, [14559]=279, [14560]=484, [14561]=245, + [14562]=1039, [14563]=275, [14564]=304, [14565]=809, [14566]=1010, [14567]=529, + [14568]=797, [14569]=472, [14570]=1870, [14571]=496, [14572]=609, [14573]=1289, + [14574]=1568, [14575]=2, [14576]=48770, [14577]=21585, [14578]=983, [14579]=1791, + [14580]=818, [14581]=2912, [14582]=1088, [14583]=1333, [14584]=2673, [14585]=2687, + [14586]=2, [14587]=2030, [14588]=1989, [14589]=3294, [14590]=1821, [14591]=4180, + [14592]=5594, [14593]=2210, [14594]=2236, [14595]=4500, [14596]=3388, [14598]=2655, + [14599]=4663, [14600]=2675, [14601]=7307, [14602]=2995, [14603]=4383, [14604]=5986, + [14605]=6868, [14606]=2955, [14607]=5581, [14608]=3162, [14611]=32509, [14612]=32622, + [14614]=16427, [14615]=14916, [14616]=22559, [14617]=6250, [14618]=2, [14620]=10135, + [14621]=15261, [14622]=10212, [14623]=20503, [14624]=20578, [14626]=20732, [14627]=200, + [14629]=10758, [14630]=250, [14631]=16251, [14632]=21746, [14633]=16367, [14634]=625, + [14635]=750, [14636]=12476, [14637]=25048, [14638]=25145, [14639]=375, [14640]=12668, + [14641]=19074, [14642]=1, [14643]=2, [14652]=4215, [14653]=6854, [14654]=3931, + [14655]=10392, [14656]=4300, [14657]=4195, [14658]=7884, [14659]=9382, [14660]=7063, + [14661]=5461, [14662]=9413, [14663]=5141, [14664]=15058, [14665]=6214, [14666]=6367, + [14667]=10770, [14668]=13596, [14669]=9654, [14670]=20396, [14671]=13012, [14672]=7309, + [14673]=8303, [14674]=8270, [14675]=8182, [14676]=13841, [14677]=18524, [14678]=13156, + [14680]=25101, [14681]=17139, [14682]=9906, [14683]=11932, [14684]=10477, [14685]=11594, + [14686]=18327, [14687]=23357, [14688]=16742, [14706]=2, [14707]=2, [14722]=424, + [14723]=290, [14724]=253, [14725]=292, [14726]=337, [14727]=778, [14728]=485, + [14729]=870, [14730]=1223, [14742]=1118, [14743]=583, [14744]=2310, [14745]=444, + [14746]=589, [14747]=668, [14748]=1550, [14749]=1325, [14750]=1068, [14751]=3454, + [14752]=978, [14753]=2610, [14754]=1193, [14755]=1118, [14756]=1860, [14757]=3298, + [14758]=2267, [14759]=2009, [14760]=6263, [14761]=2024, [14762]=4074, [14763]=1685, + [14764]=2250, [14765]=4427, [14766]=5486, [14767]=4478, [14768]=8566, [14769]=5554, + [14770]=3172, [14771]=3030, [14772]=3284, [14773]=3052, [14774]=6252, [14775]=8367, + [14776]=6326, [14777]=9710, [14778]=4933, [14779]=13347, [14780]=14289, [14781]=4617, + [14782]=5004, [14783]=5424, [14784]=8202, [14785]=9558, [14786]=11841, [14787]=8323, + [14788]=6408, [14789]=17541, [14790]=18781, [14791]=6056, [14792]=6960, [14793]=6985, + [14794]=10563, [14795]=12690, [14796]=16983, [14797]=11427, [14798]=24037, [14799]=17308, + [14800]=25826, [14801]=8156, [14802]=9749, [14803]=9087, [14804]=16296, [14805]=20577, + [14806]=15560, [14807]=8211, [14808]=11581, [14809]=20276, [14810]=11669, [14811]=29898, + [14812]=32007, [14813]=10601, [14814]=21593, [14815]=13760, [14816]=27620, [14817]=20883, + [14818]=2, [14820]=2, [14821]=5290, [14822]=2, [14823]=2, [14824]=2, + [14825]=8595, [14826]=2311, [14827]=2320, [14828]=3772, [14829]=5049, [14830]=3801, + [14831]=4120, [14832]=2363, [14833]=2988, [14834]=2777, [14835]=8192, [14836]=2, + [14837]=2, [14838]=2892, [14839]=4701, [14840]=7925, [14841]=4284, [14842]=12483, + [14843]=5036, [14844]=11471, [14845]=2, [14846]=4534, [14847]=4254, [14848]=6854, + [14849]=8271, [14850]=10442, [14851]=7416, [14852]=19420, [14853]=4174, [14854]=16204, + [14855]=6502, [14856]=6156, [14857]=9266, [14858]=11740, [14859]=14820, [14860]=10523, + [14861]=5045, [14862]=18339, [14863]=7951, [14864]=7602, [14865]=12018, [14866]=13301, + [14867]=17802, [14868]=13109, [14869]=7577, [14870]=2, [14871]=2, [14873]=2, + [14874]=2, [14875]=2, [14876]=2, [14877]=2, [14878]=2, [14879]=3, + [14880]=2, [14881]=2, [14882]=2, [14893]=2, [14895]=4904, [14896]=3418, + [14897]=2286, [14898]=2295, [14899]=3731, [14900]=4298, [14901]=3494, [14902]=7482, + [14903]=2173, [14904]=9245, [14905]=3217, [14906]=2989, [14907]=6123, [14908]=8195, + [14909]=4897, [14910]=3033, [14911]=5327, [14912]=15232, [14913]=6697, [14914]=3590, + [14915]=11569, [14916]=17254, [14917]=3931, [14918]=3654, [14919]=7717, [14920]=9744, + [14921]=6407, [14922]=8273, [14923]=4604, [14924]=14031, [14925]=10562, [14926]=5598, + [14927]=5301, [14928]=12673, [14929]=8999, [14930]=22950, [14931]=18548, [14932]=11784, + [14933]=7884, [14934]=6754, [14935]=11885, [14936]=16702, [14937]=11406, [14938]=6470, + [14939]=6793, [14940]=4059, [14941]=2515, [14942]=2524, [14943]=2534, [14944]=4806, + [14945]=6432, [14946]=4151, [14947]=11198, [14948]=10301, [14949]=3907, [14950]=3630, + [14951]=5902, [14952]=7323, [14953]=9158, [14954]=15234, [14955]=5418, [14956]=3108, + [14957]=7223, [14958]=12938, [14959]=5143, [14960]=4870, [14961]=9259, [14962]=11690, + [14963]=8301, [14964]=21741, [14965]=4450, [14966]=17801, [14967]=6867, [14968]=6502, + [14969]=12241, [14970]=15598, [14971]=11076, [14972]=10486, [14973]=27810, [14974]=6009, + [14975]=20219, [14976]=8349, [14977]=7982, [14978]=13913, [14979]=14664, [14980]=19201, + [14981]=13764, [14982]=34119, [14983]=7985, [15003]=20, [15004]=42, [15005]=28, + [15006]=72, [15007]=24, [15008]=37, [15009]=162, [15010]=163, [15011]=183, + [15012]=296, [15013]=86, [15014]=585, [15015]=86, [15016]=200, [15017]=611, + [15018]=706, [15019]=366, [15045]=19938, [15046]=22482, [15047]=29836, [15048]=24409, + [15049]=20543, [15050]=26071, [15051]=21736, [15052]=31933, [15053]=17275, [15054]=14559, + [15055]=16019, [15056]=20966, [15057]=18728, [15058]=17631, [15059]=24776, [15060]=21277, + [15061]=13803, [15062]=25709, [15063]=11701, [15064]=19717, [15065]=22233, [15066]=23650, + [15067]=12758, [15068]=21835, [15069]=17012, [15070]=9504, [15071]=11443, [15072]=16233, + [15073]=11530, [15074]=6867, [15075]=18449, [15076]=16604, [15077]=7615, [15078]=9649, + [15079]=21760, [15080]=18230, [15081]=20261, [15082]=10375, [15083]=6872, [15084]=7311, + [15085]=22732, [15086]=13154, [15087]=17892, [15088]=9901, [15090]=22002, [15091]=7195, + [15092]=7656, [15093]=8368, [15094]=14155, [15095]=20885, [15096]=17332, [15104]=3188, + [15105]=14397, [15106]=14447, [15107]=5000, [15108]=5000, [15109]=3344, [15110]=351, + [15111]=609, [15112]=354, [15113]=1361, [15114]=372, [15115]=473, [15116]=1163, + [15117]=1219, [15118]=1562, [15119]=13683, [15120]=786, [15121]=1433, [15122]=701, + [15123]=2463, [15124]=847, [15125]=990, [15126]=1989, [15127]=1647, [15128]=2425, + [15129]=1825, [15130]=3252, [15131]=2023, [15132]=1017, [15133]=3826, [15134]=3295, + [15135]=1357, [15136]=1248, [15137]=1516, [15138]=15198, [15139]=3055, [15140]=2529, + [15141]=41937, [15142]=2603, [15143]=1439, [15144]=4232, [15145]=4943, [15146]=3730, + [15147]=1753, [15148]=1613, [15149]=1960, [15150]=2951, [15151]=3949, [15152]=4449, + [15153]=2626, [15154]=2372, [15155]=2571, [15156]=5266, [15157]=6200, [15158]=4320, + [15159]=6743, [15160]=2430, [15161]=3319, [15162]=5829, [15163]=3344, [15164]=9135, + [15165]=3744, [15166]=3945, [15167]=7484, [15168]=7951, [15169]=5542, [15170]=13103, + [15171]=8697, [15172]=4706, [15173]=5387, [15174]=6017, [15175]=9692, [15176]=12121, + [15177]=8525, [15178]=4935, [15179]=18593, [15180]=6686, [15181]=11312, [15182]=5996, + [15183]=6751, [15184]=7197, [15185]=12905, [15186]=15373, [15187]=10918, [15188]=8702, + [15189]=15722, [15190]=9614, [15191]=9034, [15192]=10595, [15193]=17586, [15194]=22412, + [15195]=23617, [15196]=2500, [15197]=2500, [15198]=10000, [15199]=10000, [15200]=5000, + [15202]=515, [15203]=621, [15204]=779, [15205]=1063, [15206]=857, [15207]=868, + [15210]=825, [15211]=1916, [15212]=3450, [15213]=8165, [15214]=11356, [15215]=16744, + [15216]=25932, [15217]=30996, [15218]=37047, [15219]=41376, [15220]=43493, [15221]=51946, + [15222]=1219, [15223]=2376, [15224]=2695, [15225]=5958, [15226]=8755, [15227]=21925, + [15228]=28308, [15229]=31924, [15230]=2755, [15231]=5033, [15232]=6112, [15233]=10476, + [15234]=11354, [15235]=21090, [15236]=27485, [15237]=30526, [15238]=36496, [15239]=42409, + [15240]=49281, [15241]=3012, [15242]=4426, [15243]=7871, [15244]=12769, [15245]=23284, + [15246]=45704, [15247]=50576, [15248]=1791, [15249]=3877, [15250]=6894, [15251]=17685, + [15252]=27906, [15253]=31547, [15254]=35584, [15255]=42545, [15256]=47531, [15257]=55234, + [15258]=64185, [15259]=3306, [15260]=11556, [15261]=13530, [15262]=21552, [15263]=28889, + [15264]=41517, [15265]=46823, [15266]=54405, [15267]=60203, [15268]=1024, [15269]=3036, + [15270]=22775, [15271]=43701, [15272]=51740, [15273]=57108, [15274]=31666, [15275]=35714, + [15276]=45263, [15277]=0, [15278]=50285, [15279]=18267, [15280]=20603, [15281]=26110, + [15282]=30628, [15283]=37365, [15284]=3039, [15285]=4061, [15286]=5568, [15287]=12516, + [15288]=33743, [15289]=41161, [15290]=0, [15291]=19591, [15292]=0, [15293]=0, + [15294]=20135, [15295]=22712, [15296]=34657, [15297]=45, [15298]=305, [15299]=42, + [15300]=86, [15301]=108, [15302]=47, [15303]=299, [15304]=414, [15305]=412, + [15306]=208, [15307]=815, [15308]=241, [15309]=220, [15310]=279, [15311]=982, + [15312]=745, [15313]=514, [15322]=7745, [15323]=17791, [15324]=25565, [15325]=31784, + [15326]=200, [15327]=200, [15329]=622, [15330]=1059, [15331]=555, [15332]=2263, + [15333]=537, [15334]=828, [15335]=580, [15336]=1669, [15337]=2027, [15338]=1387, + [15339]=2466, [15340]=1016, [15341]=1697, [15342]=2907, [15343]=1139, [15344]=2516, + [15345]=1894, [15346]=2788, [15347]=955, [15348]=792, [15349]=1549, [15350]=2384, + [15351]=1090, [15352]=4101, [15353]=3467, [15354]=1600, [15355]=1620, [15356]=3935, + [15357]=2693, [15358]=3604, [15359]=5955, [15360]=2372, [15361]=2204, [15362]=3871, + [15363]=4894, [15364]=2431, [15365]=2415, [15366]=5250, [15367]=6246, [15368]=3674, + [15369]=3097, [15370]=5439, [15371]=3467, [15372]=3383, [15373]=6930, [15374]=7363, + [15375]=5132, [15376]=8653, [15377]=3191, [15378]=4358, [15379]=7654, [15380]=4390, + [15381]=12593, [15382]=5043, [15383]=5313, [15384]=9155, [15385]=11449, [15386]=7287, + [15387]=5583, [15388]=6357, [15389]=10146, [15390]=16175, [15391]=12178, [15392]=6382, + [15393]=6479, [15394]=14615, [15395]=10379, [15396]=582, [15397]=731, [15398]=108, + [15399]=151, [15400]=109, [15401]=72, [15402]=109, [15403]=279, [15404]=337, + [15405]=245, [15406]=402, [15407]=500, [15408]=500, [15409]=1000, [15410]=5000, + [15411]=10283, [15412]=500, [15413]=22519, [15414]=1500, [15415]=500, [15416]=1000, + [15417]=500, [15418]=73597, [15419]=600, [15420]=100, [15421]=17855, [15422]=500, + [15423]=500, [15424]=1404, [15425]=8272, [15426]=13419, [15427]=8536, [15428]=8024, + [15429]=9049, [15430]=15435, [15431]=19673, [15432]=14104, [15433]=21849, [15434]=10964, + [15435]=18199, [15436]=10519, [15437]=13301, [15438]=12264, [15439]=19387, [15440]=24707, + [15441]=18595, [15442]=27433, [15443]=1119, [15444]=1404, [15445]=1127, [15449]=425, + [15450]=533, [15451]=643, [15452]=215, [15453]=269, [15455]=3541, [15456]=4442, + [15457]=1134, [15458]=1423, [15459]=1142, [15460]=1, [15461]=841, [15462]=653, + [15463]=787, [15464]=4504, [15465]=2713, [15466]=2323, [15467]=8630, [15468]=1207, + [15469]=1009, [15470]=2432, [15471]=2034, [15472]=33, [15473]=65, [15474]=23, + [15475]=24, [15476]=44, [15477]=192, [15478]=52, [15479]=179, [15480]=84, + [15481]=127, [15482]=54, [15483]=41, [15484]=68, [15485]=344, [15486]=245, + [15487]=479, [15488]=967, [15489]=363, [15490]=105, [15491]=211, [15492]=212, + [15493]=647, [15494]=693, [15495]=220, [15496]=461, [15497]=387, [15498]=674, + [15499]=307, [15500]=1377, [15501]=269, [15502]=411, [15503]=1091, [15504]=1168, + [15505]=561, [15506]=723, [15507]=364, [15508]=365, [15509]=485, [15510]=423, + [15511]=1474, [15512]=1578, [15513]=1264, [15514]=1902, [15515]=844, [15516]=1587, + [15517]=850, [15518]=2825, [15519]=607, [15520]=879, [15521]=1942, [15522]=2516, + [15523]=1474, [15524]=2876, [15525]=1634, [15526]=704, [15527]=1020, [15528]=930, + [15529]=2487, [15530]=3222, [15531]=2076, [15532]=944, [15533]=2290, [15534]=2308, + [15535]=1270, [15536]=4107, [15537]=1163, [15538]=1412, [15539]=1282, [15540]=2827, + [15541]=3441, [15542]=2365, [15543]=4179, [15544]=2448, [15545]=1348, [15546]=4793, + [15547]=1234, [15548]=1649, [15549]=1504, [15550]=3316, [15551]=4438, [15552]=5226, + [15553]=3061, [15554]=1853, [15555]=2802, [15556]=1697, [15557]=5486, [15558]=4128, + [15559]=1450, [15560]=1937, [15561]=4705, [15562]=3558, [15563]=5562, [15564]=1000, + [15565]=4274, [15566]=2178, [15567]=6666, [15568]=1813, [15569]=7162, [15570]=2431, + [15571]=2218, [15572]=4714, [15573]=5840, [15574]=5150, [15575]=2300, [15576]=4462, + [15577]=2317, [15578]=6962, [15579]=2122, [15580]=4871, [15581]=2794, [15582]=6544, + [15583]=5344, [15584]=7594, [15585]=1446, [15587]=1822, [15588]=2194, [15589]=5462, + [15590]=2674, [15591]=8739, [15592]=8465, [15593]=5532, [15594]=2520, [15595]=2950, + [15596]=6909, [15597]=5642, [15598]=2763, [15599]=6631, [15600]=3787, [15601]=10345, + [15602]=7211, [15603]=3546, [15604]=11158, [15605]=3858, [15606]=3585, [15607]=9305, + [15608]=7035, [15609]=11806, [15610]=4354, [15611]=4369, [15612]=4285, [15613]=3688, + [15614]=7026, [15615]=7584, [15616]=10151, [15617]=7107, [15618]=11782, [15619]=5987, + [15620]=5564, [15621]=15766, [15622]=14834, [15623]=10715, [15624]=4971, [15625]=5818, + [15626]=8798, [15627]=14493, [15628]=9570, [15629]=6374, [15630]=10312, [15631]=16672, + [15632]=4997, [15633]=16210, [15634]=11441, [15635]=5896, [15636]=6333, [15637]=14557, + [15638]=9614, [15639]=7040, [15640]=20233, [15641]=7587, [15642]=12162, [15643]=6675, + [15644]=8130, [15645]=14577, [15646]=19506, [15647]=13125, [15648]=22212, [15649]=7808, + [15650]=22230, [15651]=15785, [15652]=7139, [15653]=8053, [15654]=8569, [15655]=19862, + [15656]=13366, [15657]=22625, [15658]=17850, [15659]=9508, [15660]=26315, [15661]=9036, + [15662]=10801, [15663]=10226, [15664]=19071, [15665]=25520, [15666]=18375, [15667]=28785, + [15668]=11697, [15669]=28538, [15670]=20455, [15671]=9782, [15672]=12164, [15673]=11075, + [15674]=18464, [15675]=28932, [15676]=25928, [15677]=19605, [15678]=21693, [15679]=13108, + [15680]=31983, [15681]=11406, [15682]=14610, [15683]=12666, [15684]=23178, [15685]=29539, + [15686]=22331, [15687]=32493, [15689]=4040, [15690]=4912, [15691]=6909, [15692]=6935, + [15693]=14533, [15694]=13264, [15695]=5771, [15697]=2825, [15698]=4273, [15699]=25, + [15702]=7396, [15703]=9032, [15704]=6145, [15705]=33830, [15706]=33830, [15707]=7215, + [15708]=8965, [15709]=7270, [15723]=2825, [15724]=4000, [15725]=3000, [15726]=3000, + [15727]=6000, [15728]=3000, [15729]=3000, [15730]=3000, [15731]=3500, [15732]=3500, + [15733]=3500, [15734]=3500, [15735]=3500, [15737]=4000, [15738]=10000, [15739]=4000, + [15740]=4000, [15741]=4000, [15742]=4000, [15743]=5000, [15744]=5000, [15745]=5000, + [15746]=5000, [15747]=5000, [15748]=5000, [15749]=5000, [15751]=5000, [15752]=5000, + [15753]=5000, [15754]=5000, [15755]=5500, [15756]=5500, [15757]=5500, [15758]=5500, + [15759]=5500, [15760]=5500, [15761]=6250, [15762]=6250, [15763]=6250, [15764]=6250, + [15765]=7500, [15768]=7500, [15769]=7500, [15770]=7500, [15771]=7500, [15772]=7500, + [15773]=10000, [15774]=10000, [15775]=10000, [15776]=10000, [15777]=15000, [15778]=1250, + [15779]=15000, [15780]=30000, [15781]=15000, [15782]=43142, [15783]=43292, [15784]=11541, + [15786]=18826, [15787]=22592, [15789]=9574, [15791]=7172, [15792]=14154, [15793]=50, + [15794]=1, [15795]=7631, [15796]=12063, [15797]=7530, [15798]=50, [15799]=7108, + [15800]=36082, [15801]=35860, [15802]=11648, [15804]=9574, [15805]=14662, [15806]=51273, + [15807]=58, [15808]=728, [15809]=2938, [15810]=2029, [15811]=5426, [15812]=9574, + [15813]=10113, [15814]=41911, [15815]=11860, [15822]=11290, [15823]=9191, [15824]=15061, + [15825]=19302, [15827]=22592, [15846]=7500, [15853]=51418, [15854]=64502, [15855]=7888, + [15856]=8788, [15857]=15335, [15858]=7530, [15859]=11296, [15860]=7907, [15861]=14969, + [15862]=28789, [15863]=28896, [15864]=1947, [15865]=6562, [15866]=562, [15867]=7464, + [15869]=50, [15870]=300, [15871]=625, [15872]=625, [15873]=8144, [15887]=29581, + [15890]=25831, [15891]=2014, [15892]=3582, [15893]=1082, [15894]=1802, [15895]=53, + [15902]=20000, [15903]=1623, [15904]=4341, [15905]=426, [15906]=427, [15907]=1647, + [15909]=4421, [15910]=2, [15912]=1148, [15918]=4848, [15925]=439, [15926]=802, + [15927]=1864, [15928]=2387, [15929]=4396, [15930]=9146, [15931]=10646, [15932]=527, + [15933]=620, [15934]=2596, [15935]=3323, [15936]=8950, [15937]=7922, [15938]=9550, + [15939]=10455, [15940]=10957, [15941]=12345, [15942]=12757, [15943]=27752, [15944]=512, + [15945]=837, [15946]=802, [15947]=1887, [15962]=2716, [15963]=4887, [15964]=6364, + [15965]=7996, [15966]=9137, [15967]=10304, [15968]=802, [15969]=425, [15970]=587, + [15971]=1064, [15972]=1171, [15973]=2313, [15974]=1461, [15975]=2421, [15976]=4146, + [15977]=4614, [15978]=5385, [15979]=7215, [15980]=7469, [15981]=7215, [15982]=8142, + [15983]=8387, [15984]=9614, [15985]=9887, [15986]=10813, [15987]=10996, [15988]=12471, + [15989]=12498, [15990]=6850, [15991]=29230, [15992]=250, [15993]=1500, [15994]=2500, + [15995]=19739, [15996]=2500, [15997]=10, [15999]=9002, [16000]=3750, [16004]=29152, + [16005]=1250, [16006]=10000, [16007]=40625, [16008]=11739, [16009]=5930, [16022]=40000, + [16023]=10000, [16026]=7160, [16027]=9488, [16028]=9488, [16029]=4828, [16030]=7270, + [16031]=7297, [16033]=4901, [16034]=7116, [16035]=7407, [16036]=9488, [16037]=9488, + [16038]=7116, [16039]=53598, [16040]=4000, [16041]=3000, [16042]=3000, [16043]=3000, + [16044]=4000, [16045]=4000, [16046]=4000, [16047]=4000, [16048]=4000, [16049]=5000, + [16050]=5000, [16051]=5000, [16052]=5000, [16053]=5000, [16054]=6000, [16055]=6000, + [16056]=6000, [16057]=5000, [16058]=10156, [16059]=100, [16060]=100, [16072]=2500, + [16073]=5000, [16082]=5000, [16083]=2500, [16084]=2500, [16085]=5000, [16110]=3000, + [16111]=3000, [16112]=550, [16113]=1250, [16166]=1, [16167]=6, [16168]=100, + [16169]=62, [16170]=25, [16171]=200, [16202]=0, [16203]=0, [16204]=0, + [16206]=300000, [16207]=1250, [16214]=3000, [16215]=3000, [16216]=3000, [16217]=3000, + [16218]=3500, [16219]=3500, [16220]=4000, [16221]=4000, [16222]=5000, [16223]=5000, + [16224]=5000, [16242]=5500, [16243]=5500, [16244]=6000, [16245]=6000, [16246]=6000, + [16247]=6000, [16248]=6000, [16249]=7500, [16250]=7500, [16251]=7500, [16252]=7500, + [16253]=7500, [16254]=7500, [16255]=7500, [16302]=25, [16315]=805, [16316]=375, + [16317]=1250, [16318]=2500, [16319]=3500, [16320]=6000, [16321]=25, [16322]=225, + [16323]=1000, [16324]=2500, [16325]=3750, [16326]=225, [16327]=750, [16328]=2000, + [16329]=3000, [16330]=5000, [16331]=150, [16335]=10000, [16336]=2917, [16337]=7491, + [16339]=0, [16341]=4285, [16342]=8830, [16345]=49483, [16346]=500, [16347]=1500, + [16348]=2750, [16349]=3750, [16350]=6500, [16351]=300, [16352]=750, [16353]=1750, + [16354]=2750, [16355]=3500, [16356]=5500, [16357]=375, [16358]=1000, [16359]=2000, + [16360]=2750, [16361]=3750, [16362]=6000, [16363]=750, [16364]=2250, [16365]=3500, + [16366]=6500, [16367]=4810, [16368]=1250, [16369]=8416, [16370]=4865, [16371]=2250, + [16372]=3000, [16373]=4500, [16374]=6500, [16375]=625, [16376]=2000, [16377]=3250, + [16378]=6000, [16379]=1000, [16380]=1750, [16381]=2500, [16382]=3250, [16383]=5000, + [16384]=1750, [16385]=2750, [16386]=3500, [16387]=5500, [16388]=2250, [16389]=4500, + [16390]=2750, [16391]=5652, [16392]=10638, [16393]=10676, [16394]=6171, [16395]=6359, + [16396]=7388, [16397]=7414, [16398]=6427, [16399]=6450, [16400]=7768, [16401]=13596, + [16402]=7823, [16403]=8223, [16404]=4754, [16405]=8286, [16406]=5545, [16407]=4809, + [16409]=8413, [16410]=5630, [16411]=5013, [16412]=5031, [16413]=11692, [16414]=11735, + [16415]=8833, [16416]=8864, [16417]=14827, [16418]=11160, [16419]=14934, [16420]=11239, + [16421]=15039, [16422]=15092, [16423]=10278, [16424]=10317, [16425]=16571, [16426]=16635, + [16427]=12923, [16428]=12914, [16429]=8641, [16430]=11564, [16431]=11606, [16432]=8736, + [16433]=11691, [16434]=8800, [16435]=11775, [16436]=8863, [16437]=17523, [16438]=8749, + [16439]=8779, [16440]=11807, [16441]=20575, [16442]=23780, [16443]=25692, [16444]=19342, + [16445]=10429, [16446]=21041, [16447]=10506, [16448]=14132, [16449]=24631, [16450]=28472, + [16451]=24811, [16452]=33202, [16453]=33324, [16454]=14444, [16455]=25172, [16456]=29098, + [16457]=25352, [16458]=10934, [16459]=20492, [16460]=10233, [16461]=12326, [16462]=24982, + [16463]=16644, [16464]=12466, [16465]=29117, [16466]=38968, [16467]=33788, [16468]=29576, + [16469]=8466, [16470]=8497, [16471]=11429, [16472]=17205, [16473]=26654, [16474]=20063, + [16475]=23793, [16476]=18757, [16477]=25107, [16478]=18903, [16479]=21856, [16480]=19047, + [16481]=8216, [16482]=8247, [16483]=16642, [16484]=11135, [16485]=8511, [16486]=5695, + [16487]=5716, [16488]=4956, [16489]=8638, [16490]=11560, [16491]=11906, [16492]=8962, + [16493]=6474, [16494]=11282, [16495]=6519, [16496]=6853, [16497]=6880, [16498]=10360, + [16499]=6932, [16500]=6011, [16501]=10479, [16502]=14025, [16503]=10558, [16504]=14130, + [16505]=14184, [16506]=10676, [16507]=11002, [16508]=14723, [16509]=8866, [16510]=5931, + [16511]=5142, [16512]=5160, [16513]=11991, [16514]=9024, [16515]=12075, [16516]=8223, + [16517]=7131, [16518]=12485, [16519]=8318, [16520]=7213, [16521]=12572, [16522]=16827, + [16523]=17349, [16524]=13117, [16525]=17475, [16526]=13154, [16527]=17603, [16528]=13309, + [16529]=7657, [16530]=8896, [16531]=13452, [16532]=8960, [16533]=20504, [16534]=23700, + [16535]=27533, [16536]=18751, [16537]=8088, [16538]=8120, [16539]=16836, [16540]=11265, + [16541]=26178, [16542]=19707, [16543]=22782, [16544]=19850, [16545]=17210, [16546]=8593, + [16547]=8624, [16548]=11598, [16549]=33688, [16550]=25357, [16551]=25448, [16552]=29413, + [16553]=11013, [16554]=22217, [16555]=13819, [16556]=10351, [16557]=10390, [16558]=20964, + [16559]=10467, [16560]=14080, [16561]=24540, [16562]=24631, [16563]=32959, [16564]=28576, + [16565]=39843, [16566]=29991, [16567]=34666, [16568]=30341, [16569]=26305, [16570]=13073, + [16571]=16331, [16572]=12233, [16573]=24795, [16574]=16518, [16575]=12373, [16576]=12420, + [16577]=38681, [16578]=29117, [16579]=33662, [16580]=29466, [16582]=1, [16583]=500, + [16604]=17, [16605]=17, [16606]=17, [16607]=16, [16608]=193, [16622]=27307, + [16623]=7783, [16645]=62, [16646]=62, [16647]=62, [16648]=62, [16649]=62, + [16650]=62, [16651]=62, [16652]=62, [16653]=62, [16654]=62, [16655]=62, + [16656]=62, [16658]=1649, [16659]=856, [16660]=2200, [16661]=965, [16664]=9234, + [16666]=35962, [16667]=25778, [16668]=30541, [16669]=21995, [16670]=21027, [16671]=12586, + [16672]=14059, [16673]=13440, [16674]=34435, [16675]=21421, [16676]=14268, [16677]=24868, + [16678]=31694, [16679]=22822, [16680]=13788, [16681]=13055, [16682]=14582, [16683]=8765, + [16684]=9093, [16685]=8694, [16686]=15912, [16687]=20281, [16688]=22445, [16689]=14597, + [16690]=22616, [16691]=14005, [16692]=9372, [16693]=16335, [16694]=20820, [16695]=14925, + [16696]=9058, [16697]=8577, [16698]=16636, [16699]=21202, [16700]=24071, [16701]=15650, + [16702]=8592, [16703]=8138, [16704]=13639, [16705]=9127, [16706]=27841, [16707]=19962, + [16708]=18175, [16709]=25542, [16710]=10446, [16711]=17505, [16712]=11714, [16713]=11196, + [16714]=10602, [16715]=17766, [16716]=11620, [16717]=12244, [16718]=19354, [16719]=27192, + [16720]=21490, [16721]=30190, [16722]=8105, [16723]=8625, [16724]=9091, [16725]=13688, + [16726]=22270, [16727]=15968, [16728]=20354, [16729]=14592, [16730]=22609, [16731]=16210, + [16732]=21213, [16733]=15207, [16734]=14536, [16735]=8738, [16736]=9295, [16737]=9794, + [16738]=3893, [16739]=7300, [16740]=942, [16741]=1178, [16747]=27, [16748]=15, + [16766]=100, [16767]=750, [16768]=37500, [16769]=26616, [16788]=5095, [16789]=5992, + [16791]=1912, [16792]=8886, [16793]=4353, [16794]=2705, [16795]=27359, [16796]=33990, + [16797]=25591, [16798]=34253, [16799]=17192, [16800]=25885, [16801]=17322, [16802]=17388, + [16803]=26181, [16804]=17517, [16805]=17583, [16806]=17649, [16807]=26572, [16808]=26668, + [16809]=35690, [16810]=35821, [16811]=26962, [16812]=18040, [16813]=27866, [16814]=37286, + [16815]=33855, [16816]=25490, [16817]=17059, [16818]=27895, [16819]=17189, [16820]=43137, + [16821]=32476, [16822]=43466, [16823]=32719, [16824]=32842, [16825]=21977, [16826]=22057, + [16827]=22139, [16828]=22221, [16829]=34339, [16830]=22972, [16831]=23055, [16832]=56532, + [16833]=46439, [16834]=34949, [16835]=42315, [16836]=31859, [16837]=38550, [16838]=25682, + [16839]=25781, [16840]=25879, [16841]=51956, [16842]=39111, [16843]=52346, [16844]=39582, + [16845]=54148, [16846]=40759, [16847]=54543, [16848]=41237, [16849]=41382, [16850]=27564, + [16851]=27663, [16852]=27762, [16853]=37144, [16854]=27956, [16855]=33848, [16856]=25485, + [16857]=17054, [16858]=17119, [16859]=25778, [16860]=17249, [16861]=17786, [16862]=26778, + [16863]=17917, [16864]=17981, [16865]=36095, [16866]=27170, [16867]=36358, [16868]=27364, + [16873]=2254, [16886]=5492, [16887]=1988, [16889]=3093, [16890]=2483, [16891]=1697, + [16894]=1993, [16897]=71351, [16898]=53709, [16899]=35940, [16900]=54111, [16901]=72415, + [16902]=54507, [16903]=36472, [16904]=36606, [16905]=73480, [16906]=55305, [16907]=37004, + [16908]=55707, [16909]=69209, [16910]=34738, [16911]=34872, [16912]=42007, [16913]=28109, + [16914]=42324, [16915]=56647, [16916]=56861, [16917]=42802, [16918]=28642, [16919]=43123, + [16920]=28856, [16921]=43440, [16922]=58135, [16923]=58349, [16924]=43918, [16925]=30153, + [16926]=30260, [16927]=45551, [16928]=27573, [16929]=41521, [16930]=55575, [16931]=55790, + [16932]=41998, [16933]=28106, [16934]=28213, [16935]=42481, [16936]=42637, [16937]=64482, + [16938]=85917, [16939]=64672, [16940]=43275, [16941]=67178, [16942]=89496, [16943]=44904, + [16944]=45065, [16945]=68140, [16946]=90773, [16947]=68314, [16948]=41356, [16949]=62552, + [16950]=83355, [16951]=27889, [16952]=27996, [16953]=42155, [16954]=56415, [16955]=42472, + [16956]=28422, [16957]=43944, [16958]=58801, [16959]=29507, [16960]=29614, [16961]=44583, + [16962]=59652, [16963]=44900, [16964]=30040, [16965]=45221, [16966]=60503, [16971]=300, + [16975]=813, [16977]=1448, [16978]=1158, [16979]=28711, [16980]=25490, [16981]=271, + [16982]=26397, [16983]=28711, [16984]=41382, [16985]=847, [16986]=850, [16987]=884, + [16988]=47832, [16989]=25682, [16990]=998, [16992]=30874, [16993]=30989, [16994]=9884, + [16995]=14826, [16996]=41451, [16997]=41603, [16998]=35631, [16999]=10709, [17000]=10314, + [17001]=10314, [17002]=44825, [17003]=44982, [17004]=56429, [17005]=1632, [17006]=1966, + [17007]=11447, [17010]=2000, [17011]=2000, [17012]=1000, [17013]=17013, [17014]=13643, + [17015]=87004, [17016]=87000, [17017]=45000, [17018]=20000, [17019]=175, [17020]=250, + [17021]=175, [17022]=37500, [17023]=40000, [17024]=175, [17025]=40000, [17026]=250, + [17027]=100, [17028]=175, [17029]=250, [17030]=500, [17031]=250, [17032]=500, + [17033]=500, [17034]=50, [17035]=100, [17036]=200, [17037]=350, [17038]=500, + [17039]=8317, [17040]=2, [17041]=2, [17042]=5692, [17043]=3054, [17044]=15038, + [17045]=14846, [17046]=4796, [17047]=1313, [17048]=400, [17049]=22500, [17050]=12536, + [17051]=17500, [17052]=45000, [17053]=50000, [17054]=28788, [17055]=26747, [17056]=7, + [17057]=7, [17058]=7, [17059]=55000, [17060]=55000, [17061]=12888, [17062]=550, + [17063]=23961, [17064]=45914, [17065]=33381, [17066]=57970, [17067]=75452, [17068]=134832, + [17069]=75746, [17070]=109307, [17071]=104472, [17072]=82572, [17073]=113631, [17074]=98514, + [17075]=135266, [17076]=196438, [17077]=59741, [17078]=37203, [17082]=46146, [17102]=38689, + [17103]=111823, [17104]=179067, [17105]=102178, [17106]=87960, [17107]=37537, [17108]=24213, + [17109]=33625, [17110]=24648, [17111]=34648, [17112]=90558, [17113]=119278, [17119]=6, + [17123]=2, [17142]=159115, [17182]=332623, [17183]=6, [17184]=6, [17185]=48, + [17186]=48, [17187]=215, [17188]=453, [17189]=2408, [17190]=6921, [17192]=879, + [17193]=122311, [17194]=0, [17195]=0, [17196]=12, [17197]=10, [17198]=9, + [17199]=9, [17200]=6, [17201]=60, [17202]=0, [17203]=100000, [17204]=200000, + [17222]=300, [17223]=97074, [17282]=2, [17283]=2, [17303]=2, [17304]=2, + [17307]=2, [17344]=1, [17348]=250, [17349]=125, [17351]=250, [17352]=125, + [17382]=3, [17383]=2, [17402]=500, [17403]=37, [17404]=6, [17405]=50, + [17406]=6, [17407]=50, [17408]=100, [17413]=7750, [17414]=14750, [17422]=2, + [17462]=2, [17463]=2, [17482]=1, [17508]=7270, [17522]=2, [17523]=10758, + [17542]=2, [17562]=8736, [17563]=5049, [17564]=5866, [17565]=4744, [17566]=8863, + [17567]=11860, [17568]=11054, [17569]=8322, [17570]=8354, [17571]=11182, [17572]=11223, + [17573]=8449, [17574]=4744, [17575]=4744, [17576]=8544, [17577]=5717, [17578]=19632, + [17579]=22695, [17580]=19775, [17581]=26465, [17582]=8561, [17583]=17270, [17584]=11856, + [17585]=8878, [17586]=16206, [17587]=8093, [17588]=10887, [17589]=8155, [17590]=19051, + [17591]=19121, [17592]=25593, [17593]=22192, [17594]=8481, [17595]=4744, [17596]=5696, + [17597]=4744, [17598]=8607, [17599]=11518, [17600]=11866, [17601]=8931, [17602]=20439, + [17603]=23626, [17604]=20585, [17605]=27544, [17606]=8060, [17607]=16266, [17608]=10886, + [17609]=8154, [17610]=8352, [17611]=11179, [17612]=11221, [17613]=8447, [17614]=4744, + [17615]=4744, [17616]=8772, [17617]=5868, [17618]=17404, [17619]=8689, [17620]=11687, + [17621]=8751, [17622]=20437, [17623]=20510, [17624]=27444, [17625]=23789, [17682]=8750, + [17683]=14750, [17686]=9937, [17687]=9937, [17688]=3155, [17692]=881, [17694]=881, + [17695]=930, [17704]=9178, [17705]=32854, [17706]=950, [17707]=11631, [17708]=35, + [17709]=500, [17710]=35483, [17711]=14244, [17713]=14641, [17714]=10225, [17715]=10225, + [17716]=7500, [17717]=26425, [17718]=22630, [17719]=32106, [17720]=600, [17721]=2268, + [17722]=700, [17723]=750, [17724]=375, [17725]=750, [17728]=12458, [17730]=42951, + [17732]=10383, [17733]=34735, [17734]=10457, [17736]=9646, [17737]=10163, [17738]=33351, + [17739]=9100, [17740]=11401, [17741]=11447, [17742]=14450, [17743]=40747, [17744]=9031, + [17745]=22519, [17746]=5723, [17747]=500, [17748]=9107, [17749]=11425, [17750]=6151, + [17751]=15344, [17752]=28785, [17753]=26046, [17754]=17394, [17755]=5818, [17759]=10307, + [17766]=44863, [17767]=13610, [17768]=7891, [17769]=8673, [17770]=3851, [17771]=100000, + [17772]=7132, [17773]=7132, [17774]=6133, [17775]=7841, [17776]=7377, [17777]=10815, + [17778]=4506, [17779]=5547, [17780]=46796, [17782]=33625, [17783]=33625, [17922]=115, + [17942]=2, [17943]=32156, [17962]=4762, [17963]=213, [17964]=5512, [17965]=213, + [17966]=8750, [17967]=5000, [17968]=5000, [17969]=4773, [17982]=23961, [18002]=2, + [18022]=10709, [18023]=15538, [18042]=10, [18043]=15495, [18044]=41478, [18045]=300, + [18046]=3000, [18047]=24428, [18048]=49204, [18062]=2, [18082]=28161, [18083]=4522, + [18102]=17158, [18103]=15252, [18104]=17285, [18122]=2, [18123]=2, [18160]=50, + [18166]=1, [18167]=2, [18168]=56416, [18169]=25000, [18170]=25000, [18171]=25000, + [18172]=25000, [18173]=25000, [18182]=100000, [18202]=90576, [18203]=90905, [18204]=27370, + [18205]=33287, [18208]=29147, [18209]=1231, [18222]=1023, [18223]=1228, [18224]=864, + [18225]=140, [18226]=408, [18227]=248, [18228]=2500, [18229]=790, [18230]=509, + [18231]=361, [18232]=10000, [18233]=130, [18234]=225, [18235]=25000, [18236]=2716, + [18237]=1622, [18238]=3321, [18239]=875, [18241]=0, [18242]=0, [18243]=0, + [18244]=0, [18245]=0, [18246]=0, [18247]=0, [18248]=0, [18251]=5000, + [18252]=50000, [18253]=15, [18254]=18, [18255]=15, [18256]=1500, [18257]=50000, + [18258]=275, [18259]=7500, [18260]=7500, [18262]=1250, [18263]=16911, [18264]=25000, + [18265]=30000, [18267]=5000, [18269]=375, [18282]=66347, [18283]=25000, [18284]=375, + [18285]=4558, [18286]=2953, [18287]=50, [18288]=250, [18289]=9701, [18290]=30000, + [18291]=30000, [18292]=30000, [18293]=2, [18294]=250, [18295]=11582, [18296]=12322, + [18298]=20685, [18300]=200, [18301]=29363, [18302]=7530, [18303]=25232, [18304]=21480, + [18305]=14375, [18306]=7215, [18307]=11163, [18308]=14006, [18309]=11296, [18310]=47406, + [18311]=56644, [18312]=18073, [18313]=13694, [18314]=12100, [18315]=12100, [18316]=13117, + [18317]=12503, [18318]=22057, [18319]=20989, [18320]=21157, [18321]=49320, [18322]=16137, + [18323]=32988, [18324]=55189, [18325]=16137, [18326]=9036, [18327]=9376, [18328]=14116, + [18337]=8103, [18338]=36595, [18339]=11370, [18340]=28853, [18341]=7638, [18342]=29442, + [18343]=14615, [18344]=11587, [18345]=14727, [18346]=16345, [18347]=43067, [18348]=112651, + [18349]=12454, [18350]=13063, [18351]=8740, [18352]=28072, [18353]=55030, [18354]=17466, + [18355]=14996, [18366]=9805, [18367]=14762, [18368]=12348, [18369]=9915, [18370]=21612, + [18371]=20737, [18372]=56716, [18373]=28459, [18374]=21418, [18375]=14329, [18376]=52306, + [18377]=13076, [18378]=31384, [18379]=23791, [18380]=21135, [18381]=21635, [18382]=15971, + [18383]=10179, [18384]=16093, [18385]=21536, [18386]=20588, [18387]=9488, [18388]=41928, + [18389]=16830, [18390]=26811, [18391]=13076, [18392]=56710, [18393]=14944, [18394]=17133, + [18395]=36645, [18396]=57516, [18397]=22464, [18398]=27103, [18399]=27103, [18400]=14853, + [18402]=18662, [18403]=49060, [18404]=6714, [18405]=14744, [18406]=46030, [18407]=11139, + [18408]=11178, [18409]=11219, [18410]=45516, [18411]=13704, [18413]=17070, [18414]=30000, + [18415]=10000, [18416]=10000, [18417]=10000, [18418]=10000, [18419]=2, [18420]=71236, + [18421]=25741, [18424]=20595, [18425]=5537, [18427]=1322, [18428]=7500, [18429]=5889, + [18430]=2878, [18432]=4348, [18434]=7493, [18435]=3662, [18436]=3417, [18437]=2744, + [18438]=5000, [18440]=1288, [18441]=4178, [18442]=5000, [18443]=10000, [18444]=7500, + [18445]=5804, [18447]=2847, [18448]=8801, [18449]=4301, [18450]=15814, [18451]=9884, + [18452]=6910, [18453]=3378, [18454]=6963, [18455]=3404, [18456]=5613, [18457]=2674, + [18458]=12212, [18459]=7907, [18460]=30762, [18461]=8577, [18462]=41319, [18463]=41472, + [18464]=29135, [18475]=7907, [18476]=15149, [18477]=19768, [18478]=19768, [18479]=17791, + [18480]=11860, [18481]=51451, [18482]=30986, [18483]=37362, [18484]=53822, [18485]=34569, + [18486]=21683, [18487]=10000, [18490]=18655, [18491]=40695, [18493]=14233, [18494]=21444, + [18495]=16770, [18496]=14233, [18497]=9488, [18498]=47443, [18499]=34589, [18500]=31340, + [18502]=70127, [18503]=22521, [18504]=14126, [18505]=13076, [18506]=21340, [18507]=15692, + [18508]=17194, [18509]=23006, [18510]=20889, [18511]=20971, [18512]=4000, [18514]=15000, + [18515]=15000, [18516]=15000, [18517]=40000, [18518]=40000, [18519]=40000, [18520]=73101, + [18521]=17606, [18522]=36253, [18523]=34810, [18524]=35596, [18525]=13730, [18526]=17924, + [18527]=17988, [18528]=20465, [18529]=9963, [18530]=31331, [18531]=65383, [18532]=21629, + [18533]=10984, [18534]=71503, [18535]=30231, [18536]=38664, [18537]=66135, [18538]=96748, + [18541]=33028, [18542]=125278, [18543]=63728, [18544]=29212, [18545]=42546, [18546]=48035, + [18547]=23619, [18562]=100000, [18567]=37500, [18582]=1224473, [18583]=1111948, [18584]=1116267, + [18585]=14853, [18586]=14853, [18587]=2000, [18588]=200, [18592]=20000, [18594]=3000, + [18595]=25000, [18596]=2, [18600]=12000, [18602]=25000, [18606]=12500, [18607]=12500, + [18610]=36, [18611]=27, [18612]=34, [18627]=250, [18629]=0, [18630]=250, + [18631]=3000, [18632]=50, [18633]=6, [18634]=12500, [18635]=100, [18636]=100, + [18637]=600, [18638]=12500, [18639]=12500, [18641]=500, [18644]=2, [18645]=1500, + [18647]=450, [18648]=450, [18649]=450, [18650]=1250, [18651]=3000, [18652]=3000, + [18653]=4000, [18654]=4000, [18655]=4000, [18656]=4000, [18657]=5000, [18658]=6000, + [18660]=7500, [18661]=3000, [18662]=0, [18663]=0, [18664]=25, [18670]=0, + [18671]=45184, [18672]=17855, [18673]=28918, [18674]=22003, [18675]=25, [18676]=16080, + [18677]=12487, [18678]=26635, [18679]=25136, [18680]=37894, [18681]=15215, [18682]=25456, + [18683]=51100, [18684]=36141, [18685]=5000, [18686]=24527, [18687]=0, [18689]=16460, + [18690]=22026, [18691]=31400, [18692]=14375, [18693]=11425, [18694]=25910, [18695]=34450, + [18696]=33476, [18697]=10867, [18698]=14454, [18699]=15380, [18700]=10910, [18701]=36410, + [18702]=9695, [18706]=10031, [18709]=5758, [18710]=7224, [18711]=8700, [18712]=5821, + [18716]=18655, [18717]=68818, [18718]=16580, [18720]=16706, [18721]=14944, [18722]=10688, + [18723]=41953, [18725]=59927, [18726]=11296, [18727]=14487, [18728]=32466, [18729]=38302, + [18730]=10250, [18731]=500, [18734]=17194, [18735]=17253, [18736]=26112, [18737]=52427, + [18738]=37593, [18739]=20124, [18740]=10101, [18741]=10416, [18742]=22503, [18743]=14275, + [18744]=11371, [18745]=17228, [18754]=10461, [18755]=43129, [18756]=33420, [18757]=16162, + [18758]=54072, [18759]=67844, [18760]=30866, [18761]=41011, [18762]=27631, [18766]=0, + [18767]=0, [18772]=0, [18773]=0, [18774]=0, [18776]=0, [18777]=0, + [18778]=0, [18785]=0, [18786]=0, [18787]=0, [18788]=0, [18789]=0, + [18790]=0, [18791]=0, [18793]=0, [18794]=0, [18795]=0, [18796]=0, + [18797]=0, [18798]=0, [18802]=5000, [18803]=135594, [18805]=109275, [18806]=33761, + [18807]=25796, [18808]=22665, [18809]=21611, [18810]=40678, [18811]=32665, [18812]=32458, + [18813]=89103, [18814]=89135, [18815]=64095, [18816]=148714, [18817]=63975, [18820]=66290, + [18821]=64030, [18822]=125768, [18823]=26506, [18824]=33518, [18825]=31807, [18826]=31917, + [18827]=50048, [18828]=50225, [18829]=48836, [18830]=57227, [18831]=57448, [18832]=104089, + [18833]=34731, [18834]=3750, [18835]=34997, [18836]=35130, [18837]=35259, [18838]=48458, + [18839]=250, [18840]=48812, [18841]=275, [18842]=138646, [18843]=49339, [18844]=49516, + [18845]=3750, [18846]=3750, [18847]=50043, [18848]=50215, [18849]=3750, [18850]=3750, + [18851]=3750, [18852]=3750, [18853]=3750, [18854]=3750, [18855]=35945, [18856]=3750, + [18857]=3750, [18858]=3750, [18859]=3750, [18860]=36602, [18861]=28493, [18862]=3750, + [18863]=3750, [18864]=3750, [18865]=49684, [18866]=49861, [18867]=62542, [18868]=62763, + [18869]=56993, [18870]=31384, [18871]=59017, [18872]=30394, [18873]=59459, [18874]=59681, + [18875]=38445, [18876]=60118, [18877]=60339, [18878]=85645, [18879]=53364, [18902]=0, + [18944]=123, [18945]=100, [18948]=1589, [18949]=500, [18957]=250, [18963]=100, + [18964]=100, [18965]=100, [18966]=100, [18967]=100, [18983]=2, [18984]=5000, + [18985]=2, [18986]=5000, [19014]=2, [19015]=2, [19019]=255355, [19022]=28580, + [19024]=10031, [19026]=1, [19027]=1250, [19028]=1078, [19029]=0, [19030]=0, + [19031]=2500, [19032]=2500, [19035]=250, [19037]=5877, [19038]=11145, [19039]=7403, + [19040]=19468, [19041]=9946, [19042]=9984, [19043]=13291, [19044]=11116, [19045]=12500, + [19046]=12500, [19047]=9232, [19048]=28061, [19049]=15576, [19050]=18758, [19051]=9365, + [19052]=17621, [19053]=2, [19056]=12941, [19057]=11606, [19058]=21845, [19059]=17541, + [19060]=100, [19061]=75, [19062]=50, [19065]=17831, [19066]=100, [19067]=75, + [19068]=50, [19082]=0, [19083]=15044, [19084]=15100, [19085]=15155, [19086]=15209, + [19087]=10176, [19088]=15319, [19089]=11860, [19090]=10285, [19091]=9488, [19092]=14233, + [19093]=11860, [19094]=9488, [19095]=17912, [19096]=17912, [19097]=17912, [19098]=17912, + [19099]=63065, [19100]=63300, [19101]=79411, [19102]=79705, [19103]=63999, [19104]=64227, + [19105]=17540, [19106]=73353, [19107]=44171, [19108]=44327, [19109]=33920, [19110]=54922, + [19111]=24824, [19112]=10984, [19113]=13730, [19114]=17887, [19115]=11003, [19116]=4590, + [19117]=8915, [19118]=17344, [19119]=5803, [19120]=16283, [19121]=8585, [19123]=4114, + [19124]=8259, [19125]=6652, [19126]=4451, [19127]=12258, [19128]=13358, [19129]=8355, + [19130]=77791, [19131]=29746, [19132]=28438, [19133]=43277, [19134]=23365, [19135]=16954, + [19136]=22280, [19137]=31469, [19138]=79853, [19139]=42244, [19140]=60256, [19141]=16612, + [19142]=18903, [19143]=21283, [19144]=45973, [19145]=37033, [19146]=20183, [19147]=91453, + [19148]=25399, [19149]=21248, [19156]=34911, [19157]=31943, [19159]=21141, [19162]=27831, + [19163]=27931, [19164]=22425, [19165]=45006, [19166]=92896, [19167]=116531, [19168]=102890, + [19169]=129112, [19170]=103689, [19182]=25, [19202]=5500, [19203]=5500, [19204]=10000, + [19205]=10000, [19206]=15000, [19207]=20000, [19208]=17500, [19209]=17500, [19210]=30000, + [19211]=30000, [19212]=30000, [19214]=2, [19215]=5500, [19216]=5500, [19217]=10000, + [19218]=10000, [19219]=15000, [19220]=22500, [19221]=12, [19222]=2, [19223]=1, + [19224]=50, [19225]=200, [19227]=12500, [19228]=100000, [19230]=12500, [19231]=12500, + [19232]=12500, [19233]=12500, [19234]=12500, [19235]=12500, [19236]=12500, [19257]=100000, + [19258]=12500, [19259]=12500, [19260]=12500, [19261]=12500, [19262]=12500, [19263]=12500, + [19264]=12500, [19265]=12500, [19267]=100000, [19268]=12500, [19269]=12500, [19270]=12500, + [19271]=12500, [19272]=12500, [19273]=12500, [19274]=12500, [19275]=12500, [19276]=12500, + [19277]=100000, [19278]=12500, [19279]=12500, [19280]=12500, [19281]=12500, [19282]=12500, + [19283]=12500, [19284]=12500, [19287]=100000, [19288]=100000, [19289]=100000, [19290]=100000, + [19291]=4000, [19292]=4022, [19293]=19376, [19295]=750, [19296]=250, [19297]=110, + [19298]=50, [19299]=25, [19300]=100, [19301]=350, [19302]=12500, [19303]=12500, + [19304]=6, [19305]=25, [19306]=100, [19307]=100, [19308]=125000, [19309]=125000, + [19310]=125000, [19311]=125000, [19312]=125000, [19315]=125000, [19316]=7, [19317]=7, + [19318]=250, [19319]=87500, [19320]=87500, [19321]=159059, [19323]=312980, [19324]=251323, + [19325]=188888, [19326]=5500, [19327]=10000, [19328]=5500, [19329]=10000, [19330]=15000, + [19331]=22500, [19332]=22500, [19333]=22500, [19334]=160453, [19335]=128825, [19336]=72039, + [19337]=72039, [19339]=72039, [19340]=72039, [19341]=72039, [19342]=72039, [19343]=72039, + [19344]=72039, [19345]=72039, [19346]=130907, [19347]=152104, [19348]=84402, [19349]=98056, + [19350]=104609, [19351]=139989, [19352]=158929, [19353]=180812, [19354]=135086, [19355]=164835, + [19356]=221750, [19357]=166093, [19358]=130638, [19360]=180116, [19361]=111546, [19362]=106098, + [19363]=182148, [19364]=228517, [19365]=136928, [19366]=203059, [19367]=103461, [19368]=106574, + [19369]=25870, [19370]=38944, [19371]=88355, [19372]=41178, [19373]=65375, [19374]=26268, + [19375]=58442, [19376]=111528, [19377]=105328, [19378]=59115, [19379]=205863, [19380]=59561, + [19381]=74734, [19382]=128280, [19383]=128862, [19384]=130616, [19385]=61990, [19386]=46661, + [19387]=46826, [19388]=31329, [19389]=58954, [19390]=39443, [19391]=47496, [19392]=31776, + [19393]=47833, [19394]=39395, [19395]=103103, [19396]=33084, [19397]=107438, [19398]=40003, + [19399]=53541, [19400]=27603, [19401]=83116, [19402]=55609, [19403]=105328, [19404]=2, + [19405]=70277, [19406]=91160, [19407]=28312, [19426]=25000, [19430]=42919, [19431]=103117, + [19432]=147286, [19433]=80659, [19434]=80863, [19435]=79598, [19436]=31956, [19437]=32075, + [19438]=33805, [19439]=56552, [19440]=500, [19441]=1500, [19442]=25000, [19444]=7500, + [19445]=7500, [19446]=7500, [19447]=15000, [19448]=20000, [19449]=25000, [19485]=2, + [19486]=2, [19487]=2, [19488]=2, [19491]=25000, [19505]=12500, [19506]=12500, + [19507]=1701, [19508]=1423, [19509]=2583, [19510]=18750, [19511]=15000, [19512]=11250, + [19513]=5000, [19514]=18750, [19515]=11250, [19516]=15000, [19517]=5000, [19518]=18750, + [19519]=15000, [19520]=11250, [19521]=5000, [19522]=18750, [19523]=15000, [19524]=11250, + [19525]=5000, [19526]=18116, [19527]=9631, [19528]=4780, [19529]=2122, [19530]=17098, + [19531]=10048, [19532]=4986, [19533]=2154, [19534]=18750, [19535]=15000, [19536]=11250, + [19537]=5000, [19538]=18750, [19539]=15000, [19540]=11250, [19541]=5000, [19542]=59534, + [19543]=34982, [19544]=17358, [19545]=6969, [19546]=56143, [19547]=32996, [19548]=16376, + [19549]=7074, [19550]=54922, [19551]=32156, [19552]=15899, [19553]=6843, [19554]=54922, + [19555]=32156, [19556]=15899, [19557]=7285, [19558]=44011, [19559]=24117, [19560]=11924, + [19561]=5132, [19562]=41191, [19563]=24117, [19564]=11924, [19565]=5132, [19566]=70171, + [19567]=40195, [19568]=19874, [19569]=8554, [19570]=71229, [19571]=41856, [19572]=20772, + [19573]=8974, [19578]=17699, [19580]=9635, [19581]=4945, [19582]=24220, [19583]=14452, + [19584]=7418, [19587]=20183, [19589]=12043, [19590]=6181, [19595]=17509, [19596]=10485, + [19597]=5401, [19623]=2, [19670]=500, [19671]=500, [19682]=25035, [19683]=25129, + [19684]=18915, [19685]=31644, [19686]=15880, [19687]=15939, [19688]=31993, [19689]=24082, + [19690]=39683, [19691]=30000, [19692]=19980, [19693]=24192, [19694]=24286, [19695]=18285, + [19726]=500, [19727]=15000, [19762]=2, [19763]=1, [19764]=12500, [19765]=12500, + [19766]=12500, [19767]=2000, [19768]=2000, [19769]=12500, [19770]=12500, [19771]=12500, + [19772]=12500, [19773]=12500, [19774]=5000, [19776]=12500, [19777]=12500, [19778]=12500, + [19779]=12500, [19780]=12500, [19781]=12500, [19808]=15556, [19852]=98086, [19853]=73837, + [19854]=123502, [19855]=39665, [19856]=48628, [19857]=29966, [19859]=100605, [19861]=75990, + [19862]=65077, [19863]=61286, [19864]=102408, [19865]=102760, [19866]=86994, [19867]=87323, + [19869]=21051, [19870]=25356, [19871]=50287, [19873]=48660, [19874]=112009, [19875]=38739, + [19876]=85311, [19877]=43354, [19878]=26107, [19884]=107909, [19885]=44203, [19886]=24979, + [19887]=50147, [19888]=25165, [19889]=42099, [19890]=88292, [19891]=63628, [19892]=31925, + [19893]=55355, [19894]=17152, [19895]=34428, [19896]=85949, [19897]=25878, [19898]=55035, + [19899]=28009, [19900]=87868, [19901]=70566, [19903]=81895, [19904]=49325, [19905]=45253, + [19906]=26969, [19907]=21655, [19908]=62589, [19909]=104707, [19910]=84070, [19912]=43548, + [19913]=22142, [19914]=8750, [19915]=48826, [19916]=2, [19917]=2, [19918]=111003, + [19919]=31646, [19920]=56037, [19921]=60951, [19922]=63655, [19923]=44030, [19924]=2, + [19925]=43635, [19927]=62348, [19928]=27168, [19929]=16834, [19930]=73852, [19931]=300, + [19933]=2080, [19934]=1400, [19935]=830, [19936]=830, [19937]=780, [19938]=580, + [19943]=2000, [19944]=102729, [19945]=30936, [19946]=89871, [19947]=86528, [19948]=111303, + [19949]=111303, [19950]=111303, [19961]=70537, [19962]=88511, [19963]=91276, [19964]=73293, + [19965]=73565, [19967]=55576, [19968]=74373, [19969]=3360, [19970]=2175, [19971]=2000, + [19972]=3396, [19974]=2500, [19979]=25000, [19980]=2, [19981]=2, [19982]=9444, + [19983]=1, [19984]=11893, [19986]=11376, [19987]=2, [19988]=2, [19990]=25000, + [19991]=10000, [19992]=10000, [19993]=56783, [19998]=23211, [19999]=18639, [20000]=30000, + [20001]=12500, [20002]=1000, [20003]=31596, [20004]=1000, [20005]=31828, [20006]=8375, + [20007]=1000, [20008]=600, [20011]=12500, [20012]=12500, [20013]=12500, [20014]=12500, + [20015]=1658, [20016]=3635, [20030]=1580, [20032]=32257, [20035]=30651, [20036]=2750, + [20037]=15003, [20038]=64972, [20039]=31709, [20040]=20000, [20041]=11352, [20042]=11394, + [20043]=17613, [20044]=17677, [20045]=14784, [20046]=14836, [20047]=11911, [20048]=17931, + [20049]=17995, [20050]=27206, [20051]=27302, [20052]=20569, [20053]=20647, [20054]=16582, + [20055]=36867, [20056]=37009, [20057]=24654, [20058]=24748, [20059]=31894, [20060]=32012, + [20061]=25701, [20062]=100, [20063]=50, [20064]=75, [20065]=75, [20066]=100, + [20067]=50, [20068]=26353, [20069]=110199, [20070]=88472, [20071]=10307, [20072]=10307, + [20073]=24279, [20074]=300, [20075]=500, [20082]=24229, [20083]=40524, [20086]=350, + [20088]=10495, [20089]=5208, [20090]=1874, [20091]=14859, [20092]=7375, [20093]=2643, + [20094]=9974, [20095]=4949, [20096]=2138, [20097]=6723, [20098]=3336, [20099]=1441, + [20100]=12746, [20101]=6325, [20102]=2732, [20103]=8590, [20104]=4262, [20105]=1841, + [20106]=6946, [20107]=3201, [20108]=2075, [20109]=9787, [20110]=4857, [20111]=3162, + [20112]=12373, [20113]=6141, [20114]=2652, [20115]=8341, [20116]=4139, [20117]=1788, + [20118]=10121, [20119]=5022, [20120]=1808, [20121]=15417, [20122]=7650, [20123]=2742, + [20124]=7075, [20125]=3176, [20126]=2058, [20127]=9711, [20128]=4820, [20129]=3137, + [20130]=10000, [20131]=12500, [20132]=12500, [20134]=32440, [20150]=16775, [20151]=9858, + [20152]=1754, [20153]=4911, [20154]=25657, [20155]=15077, [20156]=7682, [20157]=2753, + [20158]=39293, [20159]=17803, [20160]=10461, [20161]=5191, [20162]=2242, [20163]=12038, + [20164]=1505, [20165]=6423, [20166]=3187, [20167]=20727, [20168]=6023, [20169]=2602, + [20170]=12274, [20171]=14029, [20172]=1802, [20173]=4202, [20174]=8530, [20175]=32243, + [20176]=25889, [20177]=11783, [20178]=2210, [20179]=6948, [20180]=3448, [20181]=17927, + [20182]=3377, [20183]=5227, [20184]=26635, [20185]=9632, [20186]=20645, [20187]=5999, + [20188]=2663, [20189]=12561, [20190]=14356, [20191]=1795, [20192]=4187, [20193]=8498, + [20194]=32123, [20195]=17545, [20196]=10309, [20197]=1834, [20198]=5134, [20199]=26818, + [20200]=7790, [20201]=2792, [20202]=15869, [20203]=39985, [20204]=11229, [20205]=6599, + [20206]=3275, [20207]=2122, [20208]=17098, [20209]=4968, [20210]=3234, [20211]=10122, + [20212]=25508, [20213]=16337, [20214]=85653, [20215]=24684, [20216]=20645, [20217]=16575, + [20218]=14590, [20219]=14641, [20220]=100919, [20222]=100, [20223]=50, [20224]=75, + [20225]=100, [20226]=50, [20227]=75, [20232]=75, [20234]=100, [20235]=50, + [20237]=75, [20238]=213369, [20239]=34271, [20240]=68808, [20241]=25902, [20242]=34665, + [20243]=100, [20244]=50, [20245]=131474, [20246]=52788, [20247]=70650, [20248]=7108, + [20249]=53380, [20250]=7888, [20251]=53777, [20252]=36933, [20253]=4000, [20254]=5000, + [20255]=12425, [20257]=30840, [20258]=75602, [20259]=15179, [20260]=42331, [20261]=15295, + [20262]=32135, [20263]=18495, [20264]=19101, [20265]=21571, [20266]=28870, [20280]=57576, + [20281]=9247, [20282]=18566, [20283]=6988, [20284]=9607, [20285]=36161, [20286]=14518, + [20287]=19426, [20288]=7108, [20289]=14676, [20290]=7888, [20291]=14781, [20292]=9889, + [20295]=31083, [20296]=12590, [20297]=46978, [20298]=63984, [20299]=128465, [20300]=44174, + [20301]=66510, [20302]=53401, [20303]=178668, [20304]=44832, [20305]=15038, [20306]=90319, + [20307]=7102, [20308]=68236, [20309]=91304, [20311]=12316, [20312]=18541, [20313]=37212, + [20314]=12448, [20315]=18739, [20316]=13985, [20317]=46790, [20318]=11742, [20319]=15038, + [20320]=23661, [20321]=7102, [20322]=17877, [20323]=23925, [20337]=85357, [20338]=35067, + [20339]=35199, [20340]=52993, [20341]=53191, [20342]=35593, [20343]=71445, [20344]=53783, + [20345]=12157, [20346]=15457, [20347]=72499, [20348]=54573, [20349]=234151, [20350]=140987, + [20351]=9144, [20352]=9179, [20353]=13822, [20354]=13875, [20355]=9285, [20356]=18641, + [20357]=14034, [20358]=12157, [20359]=15457, [20360]=18923, [20361]=14245, [20362]=59573, + [20363]=35877, [20368]=35582, [20369]=9975, [20380]=57958, [20381]=5000, [20382]=15000, + [20387]=0, [20391]=1, [20392]=1, [20400]=20000, [20406]=1929, [20407]=2581, + [20408]=1942, [20412]=2, [20417]=2, [20424]=175, [20425]=3042, [20426]=5000, + [20427]=730, [20428]=730, [20429]=5000, [20430]=2433, [20431]=5000, [20434]=3042, + [20437]=1825, [20438]=1825, [20439]=5000, [20440]=2582, [20441]=2433, [20442]=5000, + [20443]=2610, [20444]=5000, [20452]=250, [20468]=2, [20469]=12, [20475]=6250, + [20476]=16401, [20477]=16898, [20478]=33917, [20479]=34036, [20480]=17078, [20481]=17139, + [20487]=166093, [20498]=1000, [20499]=500, [20500]=2000, [20501]=2000, [20502]=21277, + [20503]=10000, [20504]=37873, [20505]=15457, [20506]=10000, [20507]=10000, [20508]=10000, + [20509]=10000, [20510]=10000, [20511]=10000, [20512]=10000, [20517]=9797, [20518]=500, + [20520]=2000, [20521]=9937, [20522]=41554, [20524]=15146, [20526]=500, [20527]=500, + [20528]=500, [20530]=12735, [20531]=750, [20532]=750, [20533]=750, [20534]=3975, + [20535]=750, [20536]=40671, [20537]=17736, [20538]=23733, [20539]=11908, [20540]=1250, + [20541]=500, [20542]=1250, [20543]=1250, [20544]=1250, [20545]=500, [20546]=10000, + [20547]=10000, [20548]=10000, [20549]=11484, [20550]=23051, [20551]=17352, [20552]=500, + [20553]=15000, [20554]=15000, [20555]=15000, [20556]=40667, [20557]=175, [20561]=1, + [20562]=1, [20563]=1, [20564]=1, [20565]=1, [20566]=1, [20567]=1, + [20568]=1, [20569]=1, [20570]=1, [20571]=1, [20572]=1, [20573]=1, + [20574]=1, [20575]=743, [20576]=350, [20577]=108080, [20578]=108499, [20579]=32675, + [20580]=109327, [20581]=137184, [20582]=62802, [20583]=4738, [20584]=4756, [20585]=4774, + [20586]=4792, [20587]=4810, [20588]=4827, [20589]=4845, [20590]=4992, [20591]=5010, + [20592]=5028, [20593]=5046, [20594]=5064, [20595]=5082, [20596]=5099, [20597]=4631, + [20598]=4649, [20599]=81672, [20600]=98660, [20601]=12, [20602]=12, [20603]=12, + [20604]=0, [20615]=29643, [20616]=21638, [20617]=68076, [20618]=22777, [20619]=34298, + [20621]=49595, [20622]=114103, [20623]=44713, [20624]=143303, [20625]=22879, [20626]=21638, + [20627]=57613, [20628]=45535, [20629]=55079, [20630]=24459, [20631]=35068, [20632]=101281, + [20633]=46353, [20634]=46518, [20635]=47424, [20636]=89103, [20637]=34030, [20638]=70220, + [20639]=46989, [20640]=4812, [20641]=4829, [20642]=7539, [20643]=7094, [20645]=21785, + [20646]=29963, [20647]=40097, [20648]=58693, [20649]=22425, [20650]=8511, [20652]=12858, + [20653]=8602, [20654]=60175, [20655]=8054, [20656]=18269, [20657]=47443, [20658]=15272, + [20659]=12263, [20660]=61543, [20661]=10295, [20662]=12400, [20663]=37336, [20664]=10628, + [20665]=27645, [20666]=52306, [20667]=13431, [20668]=33536, [20669]=70120, [20670]=15125, + [20671]=20984, [20672]=39497, [20673]=10199, [20674]=21227, [20675]=52306, [20676]=500, + [20677]=500, [20678]=500, [20679]=500, [20680]=31683, [20681]=18251, [20682]=73553, + [20683]=21028, [20684]=21028, [20685]=61617, [20686]=21028, [20687]=14019, [20688]=56644, + [20689]=26286, [20690]=14004, [20691]=24289, [20692]=13362, [20693]=11860, [20694]=17828, + [20695]=36253, [20696]=70171, [20697]=16903, [20698]=75410, [20699]=22708, [20700]=21968, + [20701]=34314, [20702]=35358, [20703]=29571, [20704]=27461, [20705]=21968, [20706]=21968, + [20707]=21968, [20708]=2, [20709]=50, [20710]=16457, [20711]=16521, [20712]=16583, + [20713]=16647, [20714]=20595, [20715]=20969, [20716]=11225, [20717]=10984, [20718]=2, + [20719]=2, [20720]=60551, [20721]=38790, [20722]=45413, [20723]=46934, [20724]=58873, + [20725]=0, [20726]=25000, [20727]=25000, [20728]=25000, [20729]=25000, [20730]=25000, + [20731]=25000, [20732]=25000, [20733]=25000, [20734]=25000, [20735]=25000, [20736]=25000, + [20738]=2, [20744]=500, [20745]=1000, [20746]=1000, [20747]=1000, [20748]=1000, + [20749]=1000, [20750]=1000, [20752]=750, [20753]=1000, [20754]=2500, [20755]=5000, + [20756]=10000, [20757]=10000, [20758]=125, [20761]=30000, [20763]=312, [20769]=2500, + [20770]=450, [20805]=12, [20808]=12, [20809]=12, [20812]=5, [20813]=4, + [20814]=400, [20815]=200, [20816]=20, [20817]=100, [20818]=625, [20820]=1064, + [20821]=1064, [20823]=837, [20824]=6250, [20826]=2207, [20827]=875, [20828]=677, + [20830]=1535, [20831]=1535, [20832]=1800, [20833]=1540, [20834]=600, [20835]=25, + [20836]=25, [20837]=26, [20838]=19, [20839]=32, [20840]=26, [20841]=16, + [20842]=5, [20843]=6, [20844]=150, [20845]=4, [20846]=5, [20847]=6, + [20848]=5, [20849]=16, [20850]=16, [20851]=17, [20852]=17, [20853]=21, + [20854]=450, [20855]=375, [20856]=375, [20857]=1, [20891]=1, [20892]=1, + [20893]=1, [20894]=1, [20895]=1, [20896]=1, [20897]=1, [20898]=1, + [20899]=1, [20900]=1, [20901]=1, [20906]=250, [20907]=300, [20908]=3, + [20909]=1010, [20910]=12, [20911]=7, [20912]=16, [20914]=7, [20915]=12, + [20916]=7, [20917]=7, [20918]=14, [20919]=14, [20920]=6, [20921]=9, + [20922]=6, [20923]=6, [20924]=12, [20925]=12, [20950]=1535, [20954]=5681, + [20955]=665, [20958]=1130, [20959]=1720, [20960]=1720, [20961]=1720, [20963]=900, + [20964]=6646, [20966]=3482, [20967]=3482, [20969]=5989, [20970]=375, [20971]=375, + [20973]=500, [20974]=625, [20975]=625, [20980]=5, [20981]=7, [20982]=7, + [20983]=7, [20985]=7, [20986]=10, [20987]=5, [20988]=5, [20989]=5, + [20990]=10, [20991]=7, [20992]=4, [20993]=6, [20994]=14, [20995]=4, + [20996]=6, [20997]=11, [20998]=4, [20999]=7, [21000]=6, [21001]=10, + [21002]=1, [21003]=3, [21004]=5, [21005]=1, [21006]=2, [21007]=4, + [21008]=3, [21009]=4, [21010]=2, [21011]=2, [21012]=2, [21013]=2, + [21014]=8, [21015]=1, [21016]=9, [21017]=2, [21018]=3, [21019]=3, + [21020]=3, [21021]=7, [21022]=3, [21023]=250, [21025]=1250, [21030]=100, + [21031]=200, [21033]=200, [21038]=0, [21039]=1, [21040]=1, [21071]=25, + [21072]=40, [21099]=125, [21113]=2, [21114]=50, [21115]=7057, [21116]=3807, + [21117]=7057, [21118]=3807, [21119]=1807, [21120]=1807, [21121]=2, [21122]=2, + [21123]=2, [21126]=206967, [21128]=168041, [21129]=2, [21131]=12, [21132]=12, + [21133]=12, [21134]=266569, [21135]=625, [21150]=2, [21151]=50, [21153]=125, + [21154]=1, [21156]=12, [21157]=1, [21162]=25, [21163]=40, [21164]=100, + [21168]=1500, [21176]=0, [21177]=37, [21178]=14231, [21179]=10709, [21180]=21612, + [21181]=21612, [21182]=10709, [21183]=25495, [21184]=14425, [21185]=10452, [21186]=14533, + [21187]=16409, [21188]=111220, [21189]=188888, [21190]=188888, [21192]=2, [21193]=40000, + [21194]=40000, [21195]=40000, [21214]=100000, [21215]=0, [21217]=125, [21218]=0, + [21219]=1250, [21222]=1750, [21223]=1500, [21224]=3000, [21225]=2500, [21226]=4000, + [21227]=7500, [21228]=2, [21235]=0, [21236]=0, [21240]=0, [21242]=164024, + [21243]=100, [21244]=165247, [21254]=0, [21266]=12, [21268]=172135, [21269]=110563, + [21272]=130949, [21273]=219003, [21275]=199583, [21277]=1250, [21278]=13212, [21279]=100000, + [21280]=100000, [21281]=100000, [21282]=100000, [21283]=100000, [21284]=100000, [21285]=100000, + [21286]=2, [21287]=100000, [21288]=100000, [21289]=100000, [21290]=100000, [21291]=100000, + [21292]=100000, [21293]=100000, [21294]=100000, [21295]=100000, [21296]=100000, [21297]=100000, + [21298]=100000, [21299]=100000, [21300]=100000, [21302]=100000, [21303]=100000, [21304]=100000, + [21306]=100000, [21307]=100000, [21311]=8473, [21312]=6377, [21313]=40000, [21316]=12407, + [21317]=11675, [21318]=7444, [21319]=9339, [21320]=22497, [21321]=0, [21322]=15108, + [21323]=0, [21324]=0, [21326]=8991, [21329]=53037, [21330]=45987, [21331]=100263, + [21332]=71529, [21333]=46519, [21334]=101407, [21335]=46868, [21336]=72615, [21337]=54661, + [21338]=47395, [21340]=5000, [21341]=20000, [21342]=80000, [21343]=104850, [21344]=48453, + [21345]=49895, [21346]=77286, [21347]=58170, [21348]=52826, [21349]=45805, [21350]=45983, + [21351]=100253, [21352]=71514, [21353]=67301, [21354]=58358, [21355]=58580, [21356]=90751, + [21357]=128176, [21358]=3000, [21359]=59459, [21360]=69081, [21361]=61483, [21362]=95240, + [21364]=134962, [21365]=75168, [21366]=86939, [21367]=87626, [21368]=105630, [21369]=5000, + [21370]=149787, [21371]=20000, [21372]=80445, [21373]=70067, [21374]=152080, [21375]=108479, + [21376]=70861, [21377]=200, [21383]=200, [21386]=12, [21387]=58153, [21388]=45619, + [21389]=99463, [21390]=70952, [21391]=46145, [21452]=147914, [21453]=35631, [21454]=53878, + [21455]=37098, [21456]=36024, [21457]=24104, [21458]=31027, [21459]=93411, [21460]=37496, + [21461]=45403, [21462]=22787, [21463]=34313, [21464]=22963, [21465]=2, [21466]=99935, + [21467]=50158, [21468]=28916, [21469]=24185, [21470]=29132, [21471]=88787, [21472]=30662, + [21473]=82878, [21474]=37936, [21475]=40611, [21476]=61136, [21477]=88503, [21478]=76570, + [21479]=20491, [21480]=29523, [21481]=26810, [21482]=40551, [21483]=75303, [21484]=33900, + [21485]=60674, [21486]=19033, [21487]=28658, [21488]=67812, [21489]=27637, [21490]=28487, + [21491]=23826, [21492]=113287, [21493]=34109, [21494]=21845, [21495]=35075, [21496]=17600, + [21497]=39919, [21498]=92593, [21499]=37165, [21500]=17850, [21501]=20262, [21502]=24409, + [21503]=16334, [21504]=111628, [21505]=111628, [21506]=111628, [21507]=111628, [21517]=42935, + [21519]=0, [21520]=151868, [21521]=137911, [21522]=142269, [21523]=142805, [21524]=1, + [21525]=1, [21526]=828085, [21527]=60872, [21529]=90788, [21530]=92311, [21531]=103030, + [21532]=58116, [21538]=1, [21539]=1, [21541]=1, [21542]=1, [21543]=1, + [21544]=1, [21546]=35, [21547]=2500, [21548]=15000, [21549]=1, [21550]=1, + [21551]=2, [21552]=5, [21553]=2, [21554]=1, [21555]=2, [21557]=6, + [21558]=6, [21559]=6, [21560]=2, [21561]=6, [21562]=6, [21563]=188888, + [21564]=1, [21565]=10000, [21566]=5000, [21567]=10000, [21568]=5000, [21569]=225, + [21570]=275, [21571]=25, [21572]=1, [21573]=2, [21574]=25, [21575]=25, + [21576]=25, [21577]=25, [21578]=25, [21579]=108030, [21580]=3, [21581]=50891, + [21582]=51078, [21583]=76906, [21584]=77194, [21585]=51655, [21586]=66525, [21587]=53413, + [21588]=80408, [21589]=12, [21590]=12, [21591]=12, [21592]=12, [21593]=12, + [21594]=61930, [21595]=12, [21596]=89033, [21597]=72651, [21598]=35753, [21599]=53835, + [21600]=54040, [21601]=79025, [21602]=46595, [21603]=121194, [21604]=37549, [21605]=47103, + [21606]=37819, [21607]=56933, [21608]=102777, [21609]=47782, [21610]=122759, [21611]=38499, + [21612]=87308, [21613]=72691, [21614]=66012, [21615]=53014, [21616]=114921, [21617]=38454, + [21618]=31727, [21619]=31846, [21620]=88661, [21621]=48118, [21622]=160986, [21623]=32315, + [21624]=48645, [21625]=83030, [21626]=97999, [21627]=46830, [21635]=186728, [21639]=45485, + [21645]=58116, [21647]=86370, [21648]=44756, [21650]=146570, [21651]=88279, [21652]=59078, + [21663]=61534, [21664]=86443, [21665]=58104, [21666]=91352, [21667]=55193, [21668]=83111, + [21669]=52140, [21670]=80303, [21671]=56044, [21672]=35158, [21673]=141169, [21674]=28340, + [21675]=35560, [21676]=57104, [21677]=96853, [21678]=99641, [21679]=230318, [21680]=79870, + [21681]=91369, [21682]=40230, [21683]=45100, [21684]=68186, [21685]=87120, [21686]=45577, + [21687]=102995, [21688]=39568, [21689]=33101, [21690]=73749, [21691]=26682, [21692]=26784, + [21693]=50412, [21694]=40482, [21695]=90277, [21696]=54379, [21697]=40937, [21698]=62117, + [21699]=57856, [21700]=90535, [21701]=38677, [21702]=78630, [21703]=162298, [21704]=39090, + [21705]=59105, [21706]=39364, [21707]=85399, [21708]=29905, [21709]=113642, [21710]=75174, + [21712]=115200, [21713]=250, [21714]=50, [21715]=120142, [21716]=50, [21717]=50, + [21718]=50, [21719]=50, [21720]=50, [21721]=3, [21722]=1250, [21723]=1250, + [21724]=175, [21725]=175, [21726]=175, [21727]=275, [21728]=275, [21729]=275, + [21730]=875, [21731]=875, [21732]=875, [21733]=4000, [21734]=4000, [21735]=4000, + [21736]=250000, [21737]=4000, [21738]=875, [21747]=75, [21748]=1500, [21752]=1250, + [21753]=5000, [21754]=4307, [21755]=4307, [21756]=1500, [21758]=1500, [21760]=1500, + [21763]=1500, [21764]=6420, [21765]=2542, [21766]=7500, [21767]=8375, [21768]=12500, + [21769]=1500, [21774]=11416, [21775]=8375, [21777]=3750, [21778]=18662, [21779]=21305, + [21784]=15000, [21789]=15000, [21790]=7767, [21791]=12503, [21792]=25218, [21794]=2, + [21795]=2, [21796]=2, [21800]=52522, [21801]=52726, [21802]=70573, [21803]=21253, + [21804]=31999, [21805]=21414, [21806]=103686, [21809]=48655, [21810]=25259, [21814]=51570, + [21815]=0, [21816]=1, [21817]=1, [21818]=1, [21819]=1, [21820]=1, + [21821]=1, [21822]=1, [21823]=1, [21829]=25, [21833]=25, [21835]=125, + [21836]=96853, [21837]=118175, [21838]=47433, [21839]=224399, [21840]=4000, [21841]=10000, + [21842]=8000, [21843]=10000, [21844]=8000, [21845]=4000, [21846]=21632, [21847]=21712, + [21848]=43584, [21849]=12261, [21850]=12306, [21851]=13059, [21852]=26926, [21853]=21336, + [21854]=29267, [21855]=30091, [21856]=147180, [21858]=20000, [21859]=31883, [21860]=24616, + [21861]=34308, [21862]=34437, [21863]=20474, [21864]=30824, [21865]=41251, [21866]=17197, + [21867]=26309, [21868]=35486, [21869]=32801, [21870]=32921, [21871]=44055, [21872]=40000, + [21873]=24089, [21874]=33398, [21875]=46852, [21876]=50000, [21877]=800, [21881]=5000, + [21882]=2500, [21884]=4000, [21885]=4000, [21886]=4000, [21887]=1250, [21888]=22875, + [21889]=27906, [21890]=42013, [21891]=99378, [21892]=10000, [21893]=10000, [21894]=10000, + [21895]=10000, [21896]=10000, [21897]=10000, [21898]=15000, [21899]=15000, [21900]=15000, + [21901]=15000, [21902]=20000, [21903]=15000, [21904]=15000, [21905]=15000, [21906]=15000, + [21907]=20000, [21908]=15000, [21909]=15000, [21910]=15000, [21911]=15000, [21912]=15000, + [21913]=15000, [21914]=15000, [21916]=15000, [21917]=15000, [21918]=15000, [21919]=15000, + [21927]=125, [21929]=2500, [21930]=2000, [21931]=250, [21932]=250, [21933]=750, + [21934]=1500, [21939]=100000, [21940]=1250, [21941]=1250, [21942]=1500, [21943]=1500, + [21944]=1875, [21945]=1875, [21947]=1875, [21948]=1875, [21949]=2500, [21952]=2500, + [21953]=3125, [21954]=2500, [21957]=3375, [21990]=1275, [21991]=3000, [21992]=5000, + [21993]=10000, [21994]=12476, [21995]=19624, [21996]=12568, [21997]=26359, [21998]=10074, + [21999]=19916, [22000]=26786, [22001]=19203, [22002]=16061, [22003]=25261, [22004]=16598, + [22005]=26102, [22006]=12034, [22007]=31889, [22008]=22864, [22009]=31970, [22010]=18432, + [22011]=18500, [22012]=12500, [22013]=29212, [22014]=333333, [22015]=14941, [22016]=28402, + [22017]=39735, [22053]=150, [22054]=150, [22055]=175, [22060]=41015, [22061]=31009, + [22062]=13180, [22063]=13227, [22064]=20800, [22065]=20873, [22066]=9623, [22067]=25501, + [22068]=18788, [22069]=26269, [22070]=12619, [22071]=12666, [22072]=26695, [22073]=19138, + [22074]=20067, [22075]=26852, [22076]=20213, [22077]=10299, [22078]=12992, [22079]=13038, + [22080]=20505, [22081]=10448, [22082]=19769, [22083]=27632, [22084]=19333, [22085]=26007, + [22086]=12431, [22087]=19552, [22088]=12524, [22089]=26266, [22090]=10038, [22091]=19845, + [22092]=26692, [22093]=19136, [22095]=19275, [22096]=30451, [22097]=30427, [22098]=19485, + [22099]=15558, [22100]=38271, [22101]=27564, [22102]=38368, [22106]=15536, [22107]=24438, + [22108]=15653, [22109]=24619, [22110]=12546, [22111]=33239, [22112]=23830, [22113]=33313, + [22137]=12, [22146]=100000, [22147]=1250, [22148]=375, [22149]=10283, [22150]=10283, + [22152]=12, [22153]=100000, [22179]=13500, [22180]=6500, [22181]=9000, [22182]=16750, + [22183]=8000, [22184]=11000, [22185]=9000, [22186]=13500, [22187]=16750, [22188]=7250, + [22189]=16750, [22190]=9000, [22191]=73128, [22194]=33521, [22195]=22885, [22196]=49624, + [22197]=14282, [22198]=67440, [22199]=3, [22202]=2500, [22203]=5000, [22204]=12403, + [22205]=8522, [22207]=14233, [22208]=57079, [22209]=12500, [22210]=2, [22211]=2, + [22212]=17311, [22213]=2, [22214]=12500, [22215]=2, [22219]=12500, [22220]=12500, + [22221]=20000, [22222]=20000, [22223]=11291, [22225]=14937, [22230]=9978, [22231]=16444, + [22232]=14933, [22233]=5837, [22234]=9713, [22240]=14972, [22241]=15739, [22242]=11489, + [22243]=250, [22244]=1000, [22245]=12375, [22246]=2500, [22247]=17874, [22248]=10000, + [22249]=30000, [22250]=250, [22251]=10000, [22252]=30000, [22253]=34450, [22254]=30655, + [22255]=32802, [22256]=8236, [22257]=36127, [22266]=43878, [22267]=17870, [22268]=66290, + [22269]=16476, [22270]=11882, [22271]=22328, [22272]=15152, [22273]=11408, [22274]=15270, + [22275]=15383, [22276]=1, [22277]=1, [22278]=1, [22279]=1, [22280]=1, + [22281]=1, [22282]=1, [22301]=23314, [22302]=17549, [22303]=22366, [22304]=10689, + [22305]=16090, [22306]=9963, [22307]=1500, [22308]=5000, [22309]=12500, [22310]=5000, + [22311]=14944, [22312]=12500, [22313]=9963, [22314]=64398, [22315]=49816, [22317]=52093, + [22318]=37362, [22319]=38664, [22320]=12, [22321]=10539, [22322]=53054, [22324]=200, + [22325]=12446, [22326]=15282, [22327]=2535, [22328]=22208, [22329]=34450, [22330]=15223, + [22331]=36253, [22332]=53683, [22333]=65383, [22334]=13782, [22335]=71251, [22336]=35150, + [22337]=16476, [22339]=15282, [22340]=65328, [22341]=2, [22342]=21968, [22343]=16476, + [22345]=15680, [22346]=2, [22347]=45728, [22348]=76507, [22377]=64976, [22378]=65211, + [22379]=65446, [22380]=65675, [22383]=84291, [22384]=80554, [22385]=25276, [22388]=15000, + [22389]=20000, [22390]=20000, [22391]=68652, [22392]=6250, [22393]=14750, [22394]=69400, + [22395]=12973, [22396]=48827, [22397]=13067, [22398]=16898, [22399]=49354, [22400]=13208, + [22401]=17078, [22402]=49885, [22403]=41953, [22404]=54793, [22405]=14944, [22406]=62270, + [22407]=18681, [22408]=42763, [22409]=27461, [22410]=17231, [22411]=16476, [22412]=16476, + [22416]=128887, [22417]=106420, [22418]=80096, [22419]=72911, [22420]=73173, [22421]=53974, + [22422]=50334, [22423]=50527, [22424]=50719, [22425]=123767, [22426]=51099, [22427]=102583, + [22428]=77225, [22429]=70300, [22430]=70562, [22431]=52055, [22433]=15282, [22436]=193311, + [22437]=159599, [22438]=111521, [22439]=101997, [22440]=102391, [22441]=75205, [22442]=75494, + [22443]=75782, [22448]=300, [22449]=335, [22451]=4000, [22452]=4000, [22456]=4000, + [22457]=4000, [22458]=37892, [22459]=350, [22460]=325, [22461]=1250, [22462]=32000, + [22463]=40000, [22464]=184892, [22465]=152688, [22466]=114949, [22467]=105109, [22468]=105504, + [22469]=77491, [22470]=79846, [22471]=80127, [22472]=20193, [22476]=165232, [22477]=136418, + [22478]=92905, [22479]=84595, [22480]=84922, [22481]=62658, [22482]=62892, [22483]=63132, + [22488]=160571, [22489]=132583, [22490]=99788, [22491]=90838, [22492]=91165, [22493]=67240, + [22494]=67481, [22495]=67721, [22496]=132173, [22497]=109114, [22498]=74316, [22499]=67669, + [22500]=67930, [22501]=50116, [22502]=51686, [22503]=51879, [22504]=126586, [22505]=104517, + [22506]=78676, [22507]=71623, [22508]=71878, [22509]=53023, [22510]=53215, [22511]=53407, + [22512]=130290, [22513]=107574, [22514]=80969, [22515]=73703, [22516]=73958, [22517]=54551, + [22518]=50917, [22519]=51109, [22521]=1500, [22522]=1500, [22525]=200, [22526]=200, + [22527]=200, [22528]=200, [22529]=200, [22530]=15000, [22531]=15000, [22532]=15000, + [22533]=15000, [22534]=20000, [22535]=25000, [22536]=25000, [22537]=25000, [22538]=50000, + [22539]=15000, [22540]=15000, [22541]=20000, [22542]=10000, [22543]=15000, [22544]=15000, + [22545]=15000, [22547]=15000, [22548]=15000, [22551]=15000, [22552]=15000, [22553]=20000, + [22554]=15000, [22555]=20000, [22556]=15000, [22557]=25000, [22558]=25000, [22559]=15000, + [22560]=15000, [22561]=15000, [22562]=12500, [22563]=17500, [22565]=15000, [22571]=62, + [22572]=400, [22573]=400, [22574]=400, [22575]=400, [22576]=400, [22577]=400, + [22578]=400, [22589]=562016, [22596]=4, [22630]=563844, [22631]=526276, [22632]=528264, + [22641]=146, [22642]=146, [22644]=12, [22645]=25, [22647]=100, [22648]=12, + [22649]=12, [22650]=12, [22651]=33147, [22652]=69171, [22654]=34846, [22655]=34973, + [22656]=47443, [22657]=33625, [22658]=53045, [22659]=33625, [22660]=24601, [22661]=89377, + [22662]=44851, [22663]=46175, [22664]=100642, [22665]=50516, [22666]=50712, [22667]=12789, + [22668]=16048, [22669]=68390, [22670]=34321, [22671]=34452, [22672]=33269, [22673]=50092, + [22676]=50650, [22678]=111303, [22679]=8750, [22680]=10709, [22681]=10709, [22682]=20000, + [22683]=22500, [22684]=30000, [22685]=30000, [22686]=37500, [22687]=30000, [22688]=64512, + [22689]=24282, [22690]=38147, [22691]=288527, [22692]=37500, [22694]=37500, [22695]=37500, + [22696]=37500, [22697]=37500, [22698]=37500, [22699]=72832, [22700]=73085, [22701]=91682, + [22702]=110409, [22703]=37500, [22704]=37500, [22705]=37500, [22707]=98660, [22709]=2, + [22710]=30, [22711]=22162, [22712]=22241, [22713]=74410, [22714]=14936, [22715]=22484, + [22716]=15043, [22718]=28410, [22720]=22889, [22721]=64030, [22722]=64030, [22724]=2, + [22725]=38790, [22728]=5000, [22729]=2000, [22730]=49596, [22731]=74394, [22732]=86443, + [22738]=2, [22739]=14750, [22740]=43935, [22741]=44092, [22742]=1, [22743]=1, + [22744]=1, [22745]=1, [22746]=12, [22747]=33526, [22748]=48441, [22749]=42217, + [22750]=42373, [22752]=34149, [22753]=34271, [22756]=33164, [22757]=24961, [22758]=25051, + [22759]=29190, [22760]=29302, [22761]=19607, [22762]=31492, [22763]=15806, [22764]=15864, + [22766]=12500, [22767]=12500, [22768]=12500, [22769]=12500, [22770]=12500, [22771]=12500, + [22772]=12500, [22773]=12500, [22774]=12500, [22775]=75, [22776]=25, [22777]=37, + [22778]=700, [22779]=700, [22783]=1387, [22784]=802, [22785]=500, [22786]=500, + [22787]=600, [22788]=750, [22789]=500, [22790]=750, [22791]=1000, [22792]=1250, + [22793]=2500, [22794]=10000, [22797]=1250, [22798]=326465, [22799]=327728, [22800]=245498, + [22801]=246440, [22802]=265185, [22803]=180171, [22804]=199392, [22806]=182204, [22807]=201633, + [22808]=207785, [22809]=260674, [22810]=142362, [22811]=157520, [22812]=211849, [22813]=239811, + [22815]=241520, [22816]=213775, [22818]=124717, [22819]=194225, [22820]=147283, [22821]=229338, + [22823]=2500, [22824]=2500, [22825]=2500, [22826]=2500, [22827]=2500, [22828]=2500, + [22829]=5000, [22830]=4000, [22831]=4000, [22832]=5000, [22833]=2500, [22834]=4000, + [22835]=6000, [22836]=5000, [22837]=5000, [22838]=5000, [22839]=5000, [22840]=6000, + [22841]=6000, [22842]=6000, [22843]=15084, [22844]=6000, [22845]=6000, [22846]=6000, + [22847]=6000, [22848]=7000, [22849]=7000, [22850]=7000, [22851]=20000, [22852]=11921, + [22853]=20000, [22854]=20000, [22855]=9536, [22856]=12192, [22857]=14751, [22858]=9827, + [22859]=9536, [22860]=9900, [22861]=20000, [22862]=9973, [22863]=7947, [22864]=8373, + [22865]=6723, [22866]=20000, [22867]=10157, [22868]=6357, [22869]=6821, [22870]=6845, + [22871]=5000, [22872]=14120, [22873]=14175, [22874]=21341, [22875]=21423, [22876]=21505, + [22877]=17987, [22878]=18055, [22879]=18123, [22880]=18191, [22881]=14605, [22882]=14660, + [22883]=14714, [22884]=14768, [22885]=14821, [22886]=14876, [22887]=22395, [22890]=10000, + [22891]=10000, [22897]=10000, [22900]=7500, [22901]=7500, [22902]=10000, [22903]=10000, + [22904]=10000, [22905]=12500, [22907]=12500, [22908]=15000, [22909]=12500, [22910]=20000, + [22911]=12500, [22912]=20000, [22913]=20000, [22914]=20000, [22915]=20000, [22917]=20000, + [22918]=20000, [22919]=25000, [22920]=15000, [22921]=15000, [22922]=25000, [22923]=25000, + [22924]=25000, [22925]=25000, [22926]=25000, [22927]=15000, [22935]=88355, [22936]=40803, + [22937]=72651, [22938]=61651, [22939]=60256, [22940]=62103, [22941]=77904, [22942]=189114, + [22943]=102777, [22947]=88355, [22951]=27, [22952]=16, [22953]=33, [22954]=91160, + [22956]=57, [22957]=57, [22958]=182, [22959]=183, [22960]=62091, [22961]=113642, + [22963]=149, [22964]=24, [22965]=20, [22966]=16, [22967]=95926, [22968]=59390, + [22969]=230, [22971]=231, [22976]=250, [22979]=403, [22980]=2493, [22981]=128862, + [22982]=1507, [22983]=62757, [22984]=1879, [22985]=407, [22986]=272, [22987]=342, + [22988]=197946, [22990]=576, [22991]=208, [22992]=261, [22993]=316, [22994]=91352, + [22995]=2448, [22996]=911, [22997]=1072, [22998]=717, [22999]=2500, [23000]=91256, + [23001]=91160, [23004]=58479, [23005]=58699, [23006]=58925, [23009]=148994, [23014]=183589, + [23017]=63014, [23018]=60256, [23019]=63466, [23020]=79607, [23021]=42608, [23023]=86443, + [23025]=60256, [23027]=91160, [23028]=98660, [23029]=72651, [23030]=59817, [23031]=60256, + [23032]=60264, [23033]=93164, [23034]=58290, [23035]=62561, [23036]=88355, [23037]=60256, + [23038]=60256, [23039]=264392, [23040]=91160, [23041]=91160, [23042]=91160, [23043]=174810, + [23044]=194870, [23045]=82578, [23046]=91160, [23047]=91160, [23048]=72651, [23049]=72651, + [23050]=86439, [23053]=86443, [23054]=278398, [23056]=280418, [23057]=102777, [23058]=88355, + [23059]=60256, [23060]=60256, [23061]=60256, [23062]=60256, [23063]=60256, [23064]=60256, + [23065]=60256, [23066]=60256, [23067]=60256, [23068]=81297, [23069]=89953, [23070]=90286, + [23071]=102741, [23072]=58039, [23073]=77614, [23075]=133426, [23077]=2500, [23078]=11912, + [23079]=2500, [23081]=13989, [23082]=16851, [23084]=11318, [23085]=22722, [23087]=22892, + [23088]=34463, [23089]=28825, [23090]=11572, [23091]=11615, [23092]=16476, [23093]=14624, + [23094]=2500, [23095]=2500, [23096]=2500, [23097]=2500, [23098]=2500, [23099]=2500, + [23100]=6898, [23101]=2500, [23103]=2500, [23104]=2500, [23105]=2500, [23106]=2500, + [23107]=2500, [23108]=2500, [23109]=2500, [23110]=2500, [23111]=2500, [23112]=2500, + [23113]=2500, [23114]=2500, [23115]=2500, [23116]=2500, [23117]=2500, [23118]=2500, + [23119]=2500, [23120]=2500, [23121]=2500, [23122]=40, [23123]=40, [23124]=59304, + [23125]=41953, [23126]=9488, [23127]=16476, [23128]=10461, [23129]=11269, [23130]=10000, + [23131]=12500, [23132]=56951, [23133]=15000, [23134]=15000, [23135]=10000, [23136]=12500, + [23137]=15000, [23138]=15000, [23139]=33837, [23140]=10000, [23141]=12500, [23142]=15000, + [23143]=15000, [23144]=10000, [23145]=12500, [23146]=15000, [23147]=15000, [23148]=10000, + [23149]=12500, [23150]=15000, [23151]=15000, [23152]=10000, [23153]=12500, [23154]=15000, + [23155]=15000, [23156]=13113, [23160]=200, [23161]=200, [23168]=2665, [23169]=3482, + [23170]=2684, [23171]=4213, [23173]=1243, [23176]=3, [23177]=10536, [23178]=4089, + [23192]=7143, [23193]=250000, [23197]=18717, [23198]=18165, [23199]=18858, [23200]=18929, + [23201]=18997, [23202]=18165, [23203]=19138, [23215]=500, [23219]=44473, [23220]=89270, + [23221]=203182, [23226]=97150, [23237]=60256, [23238]=128589, [23242]=267143, [23243]=13158, + [23244]=13205, [23251]=18384, [23252]=18536, [23253]=15438, [23254]=15495, [23255]=12443, + [23256]=12490, [23257]=15672, [23258]=16152, [23259]=19454, [23260]=19611, [23261]=13062, + [23262]=13109, [23263]=13156, [23264]=13204, [23265]=58, [23266]=98, [23267]=59, + [23272]=14169, [23273]=14222, [23274]=6651, [23275]=10013, [23276]=12826, [23277]=12873, + [23278]=15253, [23279]=10160, [23280]=8497, [23281]=12792, [23282]=6847, [23283]=10307, + [23284]=8619, [23285]=12976, [23286]=6945, [23287]=10454, [23288]=6993, [23289]=9526, + [23290]=6552, [23291]=9865, [23292]=21833, [23293]=21915, [23294]=18330, [23295]=18396, + [23296]=14771, [23297]=14826, [23298]=18600, [23299]=18666, [23300]=14987, [23301]=15042, + [23302]=15096, [23303]=15149, [23304]=15203, [23305]=15258, [23306]=18533, [23307]=18684, + [23308]=15560, [23309]=15619, [23310]=12541, [23311]=12589, [23312]=15795, [23313]=15854, + [23314]=12729, [23315]=12776, [23316]=12823, [23317]=12871, [23318]=12917, [23319]=12964, + [23320]=100000, [23321]=1, [23322]=1, [23323]=1, [23324]=1, [23325]=1, + [23328]=2, [23329]=5, [23331]=4, [23332]=3, [23333]=5, [23334]=10, + [23344]=1, [23345]=1, [23346]=9, [23347]=5, [23348]=1, [23353]=3, + [23354]=6, [23355]=200, [23356]=1, [23367]=9, [23369]=2, [23370]=38, + [23371]=48, [23372]=72, [23373]=58, [23375]=19, [23376]=11, [23377]=15, + [23380]=25, [23381]=10, [23383]=15, [23384]=15, [23385]=15, [23386]=17, + [23389]=15, [23390]=31, [23391]=25, [23392]=25, [23393]=25, [23395]=73, + [23396]=115, [23397]=23, [23398]=86, [23399]=100, [23400]=255, [23401]=802, + [23402]=577, [23403]=347, [23404]=218, [23405]=194, [23406]=368, [23407]=246, + [23408]=309, [23409]=1072, [23410]=1435, [23411]=1800, [23412]=429, [23413]=286, + [23414]=359, [23415]=726, [23423]=297, [23424]=1000, [23425]=1500, [23426]=2500, + [23427]=1250, [23429]=303, [23430]=243, [23431]=244, [23436]=30000, [23437]=30000, + [23438]=30000, [23439]=30000, [23440]=30000, [23441]=30000, [23444]=110, [23445]=2000, + [23446]=3000, [23447]=2500, [23448]=22500, [23449]=12500, [23451]=45671, [23452]=75452, + [23453]=75452, [23454]=45671, [23455]=57053, [23456]=45671, [23464]=45671, [23465]=57089, + [23466]=45671, [23467]=45671, [23468]=75452, [23469]=75452, [23473]=1, [23474]=1, + [23475]=1, [23476]=1, [23477]=1, [23478]=1, [23479]=1, [23482]=20269, + [23484]=20969, [23487]=33419, [23488]=44805, [23489]=47475, [23490]=39760, [23491]=18866, + [23493]=27683, [23494]=19615, [23497]=58164, [23498]=60063, [23499]=79583, [23502]=82647, + [23503]=85143, [23504]=68364, [23505]=70380, [23506]=28586, [23507]=58548, [23508]=28793, + [23509]=62984, [23510]=31529, [23511]=47104, [23512]=64163, [23513]=64391, [23514]=32234, + [23515]=29607, [23516]=44599, [23517]=30311, [23518]=60504, [23519]=45822, [23520]=30896, + [23521]=46159, [23522]=61891, [23523]=61628, [23524]=31103, [23525]=46837, [23526]=26912, + [23527]=54020, [23528]=750, [23529]=1000, [23530]=12000, [23531]=31259, [23532]=36818, + [23533]=36955, [23534]=47423, [23535]=55431, [23536]=55635, [23537]=37514, [23538]=37654, + [23539]=37795, [23540]=107762, [23541]=135202, [23542]=108562, [23543]=136202, [23544]=109350, + [23546]=137687, [23553]=176, [23554]=105375, [23555]=105763, [23556]=1, [23557]=132177, + [23558]=1807, [23559]=750, [23563]=68232, [23564]=80028, [23565]=91327, [23570]=1807, + [23571]=16000, [23572]=16000, [23573]=30000, [23574]=20000, [23575]=1500, [23576]=1250, + [23577]=269253, [23578]=700, [23582]=2, [23583]=2, [23585]=200, [23586]=85, + [23587]=18522, [23590]=10000, [23591]=10000, [23592]=10000, [23593]=10000, [23594]=15000, + [23595]=15000, [23596]=15000, [23597]=15000, [23598]=15000, [23599]=15000, [23600]=20000, + [23601]=15000, [23602]=15000, [23603]=15000, [23604]=20000, [23605]=15000, [23606]=15000, + [23607]=15000, [23608]=60000, [23609]=60000, [23610]=60000, [23611]=20000, [23612]=20000, + [23613]=25000, [23615]=15000, [23617]=15000, [23618]=15000, [23619]=15000, [23620]=120000, + [23621]=120000, [23622]=120000, [23623]=120000, [23624]=120000, [23625]=120000, [23626]=120000, + [23627]=120000, [23628]=120000, [23629]=120000, [23630]=120000, [23631]=120000, [23632]=120000, + [23633]=120000, [23634]=120000, [23635]=120000, [23636]=120000, [23637]=120000, [23638]=15000, + [23639]=75000, [23663]=64264, [23664]=96825, [23665]=128529, [23666]=43647, [23667]=65720, + [23668]=87950, [23676]=5, [23683]=750, [23684]=1500, [23689]=6250, [23690]=6250, + [23705]=12500, [23709]=12500, [23710]=12500, [23711]=4500, [23712]=1250, [23713]=1250, + [23730]=6500, [23731]=15000, [23734]=3750, [23736]=2500, [23737]=2500, [23742]=47216, + [23743]=2, [23745]=3750, [23746]=55725, [23747]=62413, [23748]=66312, [23755]=15000, + [23756]=10, [23758]=29667, [23761]=20534, [23762]=25324, [23763]=31774, [23764]=2500, + [23765]=2500, [23766]=20000, [23767]=5000, [23768]=250, [23769]=250, [23771]=250, + [23772]=20, [23773]=2, [23774]=30000, [23781]=500, [23782]=8000, [23783]=1500, + [23784]=10000, [23785]=30000, [23786]=12000, [23787]=12000, [23793]=6250, [23799]=20000, + [23800]=25000, [23802]=37500, [23803]=15000, [23804]=20000, [23805]=20000, [23806]=40000, + [23807]=15000, [23808]=40000, [23809]=15000, [23810]=20000, [23811]=15000, [23814]=15000, + [23815]=15000, [23816]=10000, [23819]=6000, [23820]=15000, [23821]=15000, [23822]=100000, + [23823]=100000, [23824]=32734, [23825]=17096, [23826]=3000, [23827]=4000, [23828]=30547, + [23829]=38321, [23831]=295, [23832]=295, [23835]=22500, [23836]=22500, [23838]=29454, + [23839]=44345, [23841]=3000, [23844]=666, [23848]=500, [23852]=875, [23858]=1500, + [23866]=1500, [23867]=1500, [23868]=1500, [23874]=20000, [23883]=17500, [23884]=20000, + [23885]=12500, [23887]=15000, [23888]=6000, [23909]=7, [23920]=3, [23922]=27, + [23923]=582, [23924]=17, [23931]=16, [23938]=1325, [23939]=1185, [23944]=7, + [23945]=8, [23946]=9, [23947]=14, [23948]=17, [23949]=22, [23965]=870, + [23976]=705, [23977]=498, [23978]=605, [23979]=990, [23987]=25, [23988]=30, + [23999]=2500, [24000]=15000, [24001]=12500, [24002]=15000, [24003]=15000, [24004]=2500, + [24006]=100, [24007]=100, [24008]=225, [24009]=225, [24020]=63979, [24021]=44959, + [24022]=38681, [24023]=16179, [24024]=19489, [24027]=30000, [24028]=30000, [24029]=30000, + [24030]=30000, [24031]=30000, [24032]=30000, [24033]=30000, [24035]=30000, [24036]=30000, + [24037]=30000, [24039]=30000, [24044]=81199, [24045]=13782, [24046]=40337, [24047]=30000, + [24048]=30000, [24050]=30000, [24051]=30000, [24052]=30000, [24053]=30000, [24054]=30000, + [24055]=30000, [24056]=30000, [24057]=30000, [24058]=30000, [24059]=30000, [24060]=30000, + [24061]=30000, [24062]=30000, [24063]=16619, [24064]=34963, [24065]=30000, [24066]=30000, + [24067]=30000, [24069]=84950, [24071]=78694, [24072]=6, [24073]=10283, [24074]=24491, + [24075]=24491, [24076]=25650, [24077]=27388, [24078]=27388, [24079]=38775, [24080]=39122, + [24082]=39122, [24083]=26588, [24085]=39470, [24086]=39818, [24087]=37037, [24088]=39122, + [24089]=39818, [24090]=20458, [24091]=24092, [24092]=39470, [24093]=39470, [24094]=80302, + [24095]=39470, [24096]=44030, [24097]=39470, [24098]=39470, [24101]=100000, [24102]=100000, + [24103]=29, [24104]=33, [24105]=6, [24106]=39122, [24107]=145, [24108]=121, + [24109]=48, [24110]=39122, [24111]=126, [24112]=76, [24113]=46, [24114]=39470, + [24116]=39470, [24117]=39818, [24118]=1500, [24119]=1500, [24120]=1500, [24121]=39818, + [24122]=30597, [24123]=30712, [24124]=12000, [24125]=15000, [24126]=15000, [24127]=15000, + [24128]=15000, [24129]=6, [24130]=7, [24131]=7, [24133]=14, [24134]=12, + [24135]=9, [24136]=284, [24138]=286, [24141]=23, [24142]=20, [24143]=1, + [24144]=16, [24145]=1, [24146]=1, [24150]=20984, [24151]=75303, [24154]=13782, + [24155]=80578, [24158]=30000, [24159]=15000, [24160]=15000, [24161]=35000, [24162]=35000, + [24163]=30000, [24164]=30000, [24165]=35000, [24166]=30000, [24167]=30000, [24168]=30000, + [24169]=30000, [24170]=35000, [24171]=35000, [24172]=15000, [24173]=15000, [24174]=30000, + [24175]=30000, [24176]=30000, [24177]=30000, [24178]=30000, [24179]=30000, [24180]=30000, + [24181]=30000, [24182]=30000, [24183]=30000, [24186]=50, [24188]=125, [24189]=1500, + [24190]=400, [24192]=30000, [24193]=30000, [24194]=30000, [24195]=30000, [24196]=30000, + [24197]=30000, [24198]=30000, [24199]=30000, [24200]=30000, [24201]=30000, [24202]=30000, + [24203]=30000, [24204]=30000, [24205]=30000, [24206]=30000, [24207]=30000, [24208]=30000, + [24209]=30000, [24210]=30000, [24211]=30000, [24212]=30000, [24213]=30000, [24214]=30000, + [24215]=30000, [24216]=30000, [24217]=30000, [24218]=30000, [24219]=30000, [24220]=30000, + [24222]=44210, [24231]=52, [24232]=225, [24234]=500, [24235]=800, [24241]=7, + [24242]=1500, [24243]=2250, [24245]=146, [24246]=400, [24249]=16563, [24250]=16626, + [24251]=16690, [24252]=25128, [24253]=25223, [24254]=26001, [24255]=21880, [24256]=21960, + [24257]=22040, [24258]=33181, [24259]=33297, [24260]=33417, [24261]=44716, [24262]=44876, + [24263]=45032, [24264]=33894, [24266]=30889, [24267]=31006, [24268]=100, [24270]=30000, + [24271]=4000, [24272]=4000, [24273]=20000, [24274]=50000, [24275]=20000, [24276]=50000, + [24281]=302, [24282]=4562, [24283]=38086, [24285]=375000, [24286]=125000, [24291]=500, + [24292]=15000, [24293]=15000, [24294]=90000, [24295]=90000, [24296]=20000, [24297]=20000, + [24298]=20000, [24299]=20000, [24300]=20000, [24301]=20000, [24302]=30000, [24303]=30000, + [24304]=30000, [24305]=30000, [24306]=30000, [24307]=30000, [24308]=15000, [24309]=15000, + [24310]=15000, [24311]=15000, [24312]=15000, [24313]=15000, [24314]=10000, [24316]=10000, + [24334]=798, [24338]=200, [24339]=299, [24340]=120, [24341]=90, [24342]=355, + [24343]=475, [24345]=100000, [24346]=421, [24347]=528, [24348]=636, [24349]=412, + [24350]=412, [24351]=1456, [24352]=1827, [24353]=1100, [24354]=1840, [24356]=74307, + [24357]=44738, [24359]=27182, [24360]=20465, [24361]=68476, [24362]=20621, [24363]=48302, + [24364]=48487, [24365]=17381, [24366]=31544, [24368]=500, [24376]=1807, [24378]=74821, + [24379]=20384, [24380]=51153, [24381]=51351, [24384]=69061, [24385]=19646, [24386]=20872, + [24387]=24577, [24388]=21023, [24389]=52751, [24390]=20737, [24391]=35419, [24392]=14219, + [24393]=14270, [24394]=89510, [24395]=14371, [24396]=36056, [24397]=28947, [24398]=27234, + [24401]=750, [24402]=12, [24403]=1000, [24404]=500, [24405]=500, [24406]=750, + [24407]=1000, [24408]=1000, [24413]=22693, [24417]=2, [24423]=15, [24424]=12, + [24425]=10, [24429]=2000, [24430]=184, [24431]=152, [24432]=172, [24433]=104, + [24434]=174, [24435]=24, [24436]=13, [24437]=11, [24438]=122, [24439]=153, + [24440]=185, [24441]=18, [24445]=43, [24446]=36, [24447]=67, [24448]=150, + [24449]=146, [24450]=15352, [24451]=23107, [24452]=17487, [24453]=70222, [24454]=21148, + [24455]=35379, [24456]=49720, [24457]=37361, [24458]=25193, [24459]=21551, [24460]=32466, + [24461]=90474, [24462]=10283, [24463]=39226, [24464]=75131, [24465]=45241, [24466]=37833, + [24476]=100, [24477]=100, [24478]=1000, [24479]=1000, [24481]=29723, [24507]=870, + [24508]=1185, [24510]=990, [24511]=1325, [24516]=870, [24517]=990, [24519]=1185, + [24521]=1325, [24540]=1000, [24575]=10617, [24576]=3666, [24577]=5520, [24578]=3693, + [24580]=5579, [24582]=16765, [24583]=22431, [24584]=11255, [24585]=15332, [24586]=20522, + [24587]=15449, [24588]=10339, [24589]=10715, [24590]=16134, [24591]=21592, [24592]=11130, + [24593]=16757, [24594]=22423, [24595]=16878, [24596]=11293, [24597]=11690, [24598]=17597, + [24599]=23547, [24600]=11815, [24601]=17787, [24602]=23798, [24603]=17912, [24604]=11983, + [24605]=11214, [24606]=16885, [24607]=22600, [24608]=11655, [24609]=17546, [24610]=23482, + [24611]=17677, [24612]=11828, [24613]=12221, [24614]=18399, [24615]=24622, [24616]=12356, + [24617]=18599, [24618]=24888, [24619]=18733, [24620]=12534, [24621]=12938, [24622]=19477, + [24623]=26061, [24624]=12158, [24625]=18306, [24626]=24501, [24627]=18445, [24628]=12341, + [24629]=12733, [24630]=19171, [24631]=25656, [24632]=12874, [24633]=19382, [24634]=25938, + [24635]=19524, [24636]=13062, [24637]=13465, [24638]=20271, [24639]=27126, [24640]=12642, + [24641]=19036, [24642]=25478, [24643]=19180, [24644]=12835, [24645]=13224, [24646]=19911, + [24647]=26646, [24648]=13373, [24649]=20134, [24650]=26946, [24651]=20282, [24652]=13571, + [24653]=13972, [24654]=21035, [24655]=28146, [24656]=14491, [24657]=21814, [24658]=26412, + [24659]=19886, [24660]=13308, [24661]=13695, [24662]=20619, [24663]=27597, [24664]=13851, + [24665]=20855, [24666]=27910, [24667]=21011, [24668]=14060, [24669]=14458, [24670]=21765, + [24671]=29128, [24672]=15003, [24673]=22583, [24674]=30219, [24675]=22745, [24676]=15217, + [24677]=15634, [24678]=21298, [24679]=28507, [24680]=14309, [24681]=21544, [24682]=28835, + [24683]=21709, [24684]=14528, [24685]=14921, [24686]=22467, [24687]=30069, [24688]=15495, + [24689]=23324, [24690]=31212, [24691]=23494, [24692]=15717, [24693]=13872, [24694]=20882, + [24695]=27942, [24696]=14019, [24697]=21103, [24698]=25553, [24699]=19239, [24700]=12874, + [24701]=13343, [24702]=20091, [24703]=26891, [24704]=13862, [24705]=20870, [24706]=27929, + [24707]=21021, [24708]=14065, [24709]=14560, [24710]=21919, [24711]=29328, [24712]=14717, + [24713]=22154, [24714]=29645, [24715]=22311, [24716]=14926, [24717]=15435, [24718]=21026, + [24719]=28141, [24720]=14515, [24721]=21854, [24722]=29244, [24723]=22015, [24724]=14731, + [24725]=15222, [24726]=22915, [24727]=30665, [24728]=15389, [24729]=23167, [24730]=30999, + [24731]=23333, [24732]=15611, [24733]=16117, [24734]=24259, [24735]=32461, [24736]=15142, + [24737]=22797, [24738]=30511, [24739]=22970, [24740]=15371, [24741]=15857, [24742]=23875, + [24743]=31951, [24744]=16035, [24745]=24139, [24746]=32304, [24747]=24316, [24748]=16270, + [24749]=16771, [24750]=25248, [24751]=33785, [24752]=15743, [24753]=23703, [24754]=31726, + [24755]=23886, [24756]=15983, [24757]=16468, [24758]=24796, [24759]=33186, [24760]=16653, + [24761]=25074, [24762]=33557, [24763]=25261, [24764]=16902, [24765]=17401, [24766]=26197, + [24767]=35058, [24768]=17591, [24769]=27171, [24770]=36357, [24771]=24761, [24772]=16571, + [24773]=17053, [24774]=25678, [24775]=34365, [24776]=17248, [24777]=25971, [24778]=34759, + [24779]=26165, [24780]=17509, [24781]=18005, [24782]=3572, [24783]=27207, [24784]=36410, + [24785]=18754, [24786]=28229, [24787]=37774, [24788]=28431, [24789]=19021, [24790]=19543, + [24791]=26622, [24792]=35634, [24793]=17886, [24794]=26930, [24795]=36044, [24796]=27136, + [24797]=18160, [24798]=18652, [24799]=28084, [24800]=37586, [24801]=19366, [24802]=29156, + [24803]=39015, [24804]=29367, [24805]=19646, [24806]=16646, [24807]=25170, [24808]=33531, + [24809]=16823, [24810]=25324, [24811]=30664, [24812]=23190, [24813]=15449, [24814]=16011, + [24815]=24217, [24816]=32269, [24817]=16634, [24818]=25044, [24819]=33515, [24820]=25338, + [24821]=16878, [24822]=17472, [24823]=26420, [24824]=35194, [24825]=17660, [24826]=26585, + [24827]=35574, [24828]=26892, [24829]=17912, [24830]=18522, [24831]=25344, [24832]=33770, + [24833]=17418, [24834]=26225, [24835]=35093, [24836]=26535, [24837]=17677, [24838]=18266, + [24839]=27620, [24840]=36799, [24841]=18466, [24842]=27801, [24843]=37198, [24844]=28124, + [24845]=18733, [24846]=19340, [24847]=29241, [24848]=38954, [24849]=18170, [24850]=27356, + [24851]=36613, [24852]=27686, [24853]=18445, [24854]=19028, [24855]=28777, [24856]=38342, + [24857]=19242, [24858]=28967, [24859]=38764, [24860]=29310, [24861]=19524, [24862]=20125, + [24863]=30432, [24864]=40543, [24865]=18890, [24866]=28444, [24867]=38072, [24868]=28791, + [24869]=19180, [24870]=19761, [24871]=29887, [24872]=39823, [24873]=19984, [24874]=30089, + [24875]=40269, [24876]=30449, [24877]=20282, [24878]=20881, [24879]=31577, [24880]=42070, + [24881]=21660, [24882]=32606, [24883]=43628, [24884]=29845, [24885]=19886, [24886]=20463, + [24887]=30951, [24888]=41238, [24889]=20698, [24890]=31165, [24891]=41711, [24892]=31538, + [24893]=21011, [24894]=21606, [24895]=32675, [24896]=43531, [24897]=22424, [24898]=33758, + [24899]=45167, [24900]=34148, [24901]=22745, [24902]=23371, [24903]=35334, [24904]=42596, + [24905]=21380, [24906]=32195, [24907]=43088, [24908]=32584, [24909]=21709, [24910]=22300, + [24911]=33723, [24912]=44934, [24913]=23157, [24914]=34860, [24915]=46649, [24916]=35270, + [24917]=23494, [24918]=19462, [24919]=29075, [24920]=38980, [24921]=19671, [24922]=29384, + [24923]=39393, [24924]=26780, [24925]=18059, [24926]=18715, [24927]=27967, [24928]=37504, + [24929]=19445, [24930]=29055, [24931]=38957, [24932]=29269, [24933]=19732, [24934]=20426, + [24935]=30518, [24936]=40916, [24937]=20647, [24938]=30847, [24939]=41355, [24940]=31068, + [24941]=20942, [24942]=21656, [24943]=32353, [24944]=39250, [24945]=20360, [24946]=30423, + [24947]=40794, [24948]=30648, [24949]=20664, [24950]=21354, [24951]=31906, [24952]=42775, + [24953]=21588, [24954]=32255, [24955]=43246, [24956]=32487, [24957]=21902, [24958]=22611, + [24959]=33781, [24960]=45285, [24961]=21238, [24962]=31737, [24963]=42554, [24964]=31976, + [24965]=21561, [24966]=22246, [24967]=33237, [24968]=44566, [24969]=22494, [24970]=33609, + [24971]=45059, [24972]=33854, [24973]=22825, [24974]=23530, [24975]=35152, [24976]=47129, + [24977]=22081, [24978]=32994, [24979]=44247, [24980]=33249, [24981]=22421, [24982]=23099, + [24983]=34517, [24984]=46285, [24985]=23363, [24986]=34906, [24987]=46805, [24988]=35168, + [24989]=23712, [24990]=24410, [24991]=36472, [24992]=48902, [24993]=24678, [24994]=37833, + [24995]=50720, [24996]=38102, [24997]=23242, [24998]=23918, [24999]=35743, [25000]=47933, + [25001]=24193, [25002]=36152, [25003]=48479, [25004]=36428, [25005]=24560, [25006]=25256, + [25007]=37739, [25008]=50603, [25009]=25538, [25010]=39168, [25011]=52512, [25012]=39446, + [25013]=26593, [25014]=27325, [25015]=40822, [25016]=54721, [25017]=24989, [25018]=37345, + [25019]=50081, [25020]=37630, [25021]=25375, [25022]=26066, [25023]=38951, [25024]=52226, + [25025]=26361, [25026]=40449, [25027]=54227, [25028]=40740, [25029]=27467, [25030]=16529, + [25031]=17124, [25032]=17725, [25033]=18330, [25034]=18939, [25035]=19549, [25036]=20166, + [25037]=18811, [25038]=19383, [25039]=19956, [25040]=20536, [25041]=21119, [25042]=22296, + [25043]=22901, [25044]=26809, [25045]=27678, [25046]=28546, [25047]=29415, [25048]=30284, + [25049]=31154, [25050]=32023, [25051]=32891, [25052]=33760, [25053]=34629, [25054]=35498, + [25055]=36368, [25056]=37236, [25057]=38105, [25058]=26809, [25059]=27678, [25060]=28546, + [25061]=29415, [25062]=30284, [25063]=31154, [25064]=32023, [25065]=32891, [25066]=33760, + [25067]=34629, [25068]=35498, [25069]=36368, [25070]=37236, [25071]=38105, [25072]=35505, + [25073]=36784, [25074]=35384, [25075]=36601, [25076]=37822, [25077]=39054, [25078]=40295, + [25079]=41545, [25080]=42798, [25081]=44063, [25082]=45337, [25083]=46619, [25084]=47905, + [25085]=49203, [25086]=26809, [25087]=27678, [25088]=28546, [25089]=29415, [25090]=30284, + [25091]=31154, [25092]=32023, [25093]=32891, [25094]=33760, [25095]=34629, [25096]=35498, + [25097]=36368, [25098]=37236, [25099]=38105, [25100]=53097, [25101]=55022, [25102]=56961, + [25103]=58907, [25104]=60871, [25105]=62848, [25106]=66529, [25107]=68578, [25108]=70639, + [25109]=72714, [25110]=67687, [25111]=69613, [25112]=71552, [25113]=73504, [25114]=51906, + [25115]=53793, [25116]=55694, [25117]=57607, [25118]=59526, [25119]=61465, [25120]=63416, + [25121]=65381, [25122]=69140, [25123]=71176, [25124]=73225, [25125]=75279, [25126]=77354, + [25127]=79441, [25128]=70111, [25129]=72632, [25130]=68033, [25131]=70376, [25132]=72735, + [25133]=75102, [25134]=77493, [25135]=79901, [25136]=82324, [25137]=84754, [25138]=89561, + [25139]=92090, [25140]=94626, [25141]=97186, [25142]=54898, [25143]=56882, [25144]=58874, + [25146]=62906, [25147]=64942, [25148]=66984, [25149]=69045, [25150]=64360, [25151]=66273, + [25152]=68191, [25153]=70130, [25154]=74055, [25155]=76057, [25156]=67135, [25157]=69567, + [25158]=72015, [25160]=76952, [25161]=79448, [25162]=81961, [25163]=84481, [25164]=87025, + [25165]=89585, [25166]=92161, [25167]=94745, [25168]=97353, [25169]=99977, [25170]=65647, + [25171]=68031, [25172]=70431, [25174]=75271, [25175]=77719, [25176]=80183, [25177]=82663, + [25178]=85151, [25179]=87663, [25180]=90191, [25181]=92736, [25182]=95287, [25183]=97863, + [25184]=55279, [25185]=57275, [25186]=55077, [25187]=56971, [25188]=58878, [25189]=60792, + [25190]=62725, [25191]=64670, [25192]=66629, [25193]=68593, [25194]=70577, [25195]=72574, + [25196]=74583, [25197]=76598, [25198]=54088, [25199]=56046, [25200]=58017, [25201]=59995, + [25202]=61992, [25203]=59415, [25204]=61303, [25205]=63210, [25206]=65130, [25207]=67062, + [25208]=69001, [25209]=70959, [25210]=72930, [25211]=74914, [25212]=66123, [25213]=68522, + [25214]=70937, [25215]=73369, [25216]=75809, [25217]=78272, [25218]=80752, [25219]=85418, + [25220]=87987, [25221]=90572, [25222]=93173, [25223]=86680, [25224]=89096, [25225]=91528, + [25226]=64642, [25227]=66986, [25228]=69353, [25229]=71736, [25230]=74136, [25231]=76543, + [25232]=78974, [25233]=81422, [25234]=83877, [25235]=88650, [25236]=91203, [25237]=93772, + [25238]=96348, [25239]=98949, [25240]=41917, [25241]=43430, [25242]=44947, [25243]=42062, + [25244]=43473, [25245]=44893, [25246]=46318, [25247]=47758, [25248]=49207, [25249]=50665, + [25250]=52128, [25251]=55052, [25252]=56574, [25253]=58100, [25254]=41025, [25255]=42508, + [25256]=44001, [25257]=45499, [25258]=47011, [25259]=48533, [25260]=50065, [25261]=51601, + [25262]=53152, [25263]=49512, [25264]=50952, [25265]=52395, [25266]=53854, [25267]=56837, + [25268]=40132, [25269]=41586, [25270]=43050, [25271]=44524, [25272]=46003, [25273]=47496, + [25274]=48998, [25275]=50511, [25276]=52027, [25277]=53559, [25278]=55100, [25279]=56650, + [25280]=58205, [25281]=59775, [25282]=42203, [25283]=40665, [25284]=42100, [25286]=44999, + [25287]=46458, [25288]=47932, [25289]=49415, [25290]=50908, [25291]=52406, [25292]=53918, + [25293]=55439, [25294]=56971, [25295]=58506, [25296]=55080, [25297]=57071, [25298]=59068, + [25299]=56754, [25300]=58654, [25301]=60568, [25302]=62488, [25303]=64427, [25304]=66379, + [25305]=68344, [25306]=70314, [25307]=72305, [25308]=74308, [25309]=76324, [25310]=53890, + [25311]=55842, [25312]=57806, [25313]=59783, [25314]=61768, [25315]=65421, [25316]=61072, + [25317]=62967, [25318]=64880, [25319]=66806, [25320]=68745, [25321]=70690, [25322]=72654, + [25323]=74632, [25324]=65881, [25325]=68266, [25326]=70673, [25327]=73097, [25328]=75536, + [25329]=77984, [25330]=80456, [25331]=85122, [25332]=87675, [25333]=90252, [25334]=92845, + [25335]=95454, [25336]=88752, [25337]=91175, [25338]=6906, [25339]=5199, [25340]=6959, + [25341]=3763, [25342]=5666, [25343]=3791, [25344]=3805, [25345]=5729, [25346]=7668, + [25347]=5926, [25348]=7930, [25349]=9234, [25350]=4633, [25351]=6974, [25352]=4666, + [25353]=4683, [25354]=7049, [25355]=9432, [25356]=6424, [25357]=9264, [25358]=4650, + [25359]=7001, [25360]=4685, [25361]=4703, [25362]=7081, [25363]=9733, [25364]=7327, + [25365]=10921, [25366]=5480, [25367]=8286, [25368]=5520, [25369]=8310, [25370]=5559, + [25371]=11158, [25372]=8436, [25373]=12108, [25374]=6075, [25375]=9185, [25376]=5536, + [25377]=8337, [25378]=5578, [25379]=11508, [25380]=8702, [25381]=6313, [25382]=9432, + [25383]=6359, [25384]=12694, [25385]=6406, [25386]=9571, [25387]=12832, [25388]=9640, + [25389]=7001, [25390]=10460, [25391]=7052, [25392]=14074, [25393]=7102, [25394]=10609, + [25395]=13226, [25396]=9937, [25397]=18352, [25398]=23027, [25399]=18490, [25400]=23197, + [25401]=18626, [25402]=23370, [25403]=18765, [25404]=23540, [25405]=14176, [25406]=14227, + [25408]=870, [25409]=1185, [25410]=990, [25411]=1325, [25412]=870, [25413]=1185, + [25414]=990, [25415]=1325, [25416]=200, [25417]=870, [25418]=1185, [25419]=12, + [25420]=990, [25421]=1325, [25422]=12, [25423]=12, [25424]=12, [25425]=870, + [25426]=1185, [25427]=990, [25428]=1325, [25429]=870, [25430]=1185, [25431]=990, + [25432]=1325, [25433]=200, [25434]=870, [25435]=1185, [25436]=990, [25437]=1325, + [25438]=750, [25439]=250, [25440]=870, [25441]=1185, [25442]=990, [25443]=1325, + [25444]=870, [25445]=1185, [25446]=990, [25447]=1325, [25450]=870, [25451]=1185, + [25452]=990, [25453]=1325, [25454]=990, [25455]=1325, [25456]=870, [25457]=1185, + [25463]=500, [25464]=3301, [25466]=4, [25467]=4, [25469]=6500, [25478]=21595, + [25479]=27828, [25480]=12358, [25481]=21829, [25482]=28132, [25483]=12491, [25484]=12536, + [25485]=22143, [25486]=15782, [25487]=30284, [25488]=30284, [25489]=17320, [25492]=60005, + [25494]=75559, [25495]=60671, [25496]=45671, [25498]=25, [25499]=30284, [25500]=30284, + [25501]=18603, [25502]=21275, [25503]=12132, [25504]=15219, [25505]=29415, [25506]=28403, + [25507]=31671, [25508]=29554, [25510]=23820, [25511]=41842, [25512]=35999, [25513]=22581, + [25514]=25751, [25515]=21444, [25516]=17218, [25517]=30284, [25518]=30665, [25519]=61556, + [25521]=3000, [25522]=32604, [25523]=18733, [25524]=21827, [25525]=18032, [25526]=15000, + [25530]=32096, [25534]=18650, [25536]=73751, [25537]=92529, [25538]=74294, [25540]=23032, + [25541]=36689, [25542]=36689, [25543]=64630, [25544]=48660, [25545]=65130, [25553]=30, + [25556]=40712, [25557]=35683, [25558]=20500, [25559]=27343, [25560]=24806, [25561]=28859, + [25562]=38775, [25563]=38775, [25564]=38775, [25565]=24421, [25566]=23009, [25567]=19684, + [25568]=39519, [25569]=46281, [25570]=24884, [25574]=27656, [25575]=31225, [25576]=17410, + [25577]=27953, [25578]=17533, [25579]=31672, [25583]=16154, [25584]=19460, [25585]=45576, + [25589]=35575, [25591]=17097, [25592]=20590, [25593]=36095, [25594]=31248, [25595]=27753, + [25597]=21431, [25598]=18330, [25599]=15329, [25600]=15382, [25601]=18524, [25602]=19739, + [25603]=90253, [25605]=18690, [25606]=32891, [25607]=32891, [25608]=94489, [25609]=18967, + [25610]=28044, [25611]=10742, [25612]=33757, [25613]=12711, [25614]=40408, [25615]=33543, + [25616]=24091, [25617]=24178, [25618]=19411, [25619]=7000, [25620]=7000, [25621]=18241, + [25622]=80542, [25623]=22766, [25624]=41549, [25628]=7000, [25629]=49598, [25630]=19910, + [25631]=19983, [25632]=50141, [25633]=7000, [25634]=7000, [25636]=19393, [25637]=12977, + [25639]=57838, [25640]=58059, [25643]=23486, [25644]=23574, [25645]=23660, [25649]=250, + [25650]=7500, [25651]=22500, [25652]=22500, [25653]=17500, [25654]=17436, [25655]=26106, + [25656]=36782, [25657]=39930, [25659]=30312, [25660]=38360, [25661]=17727, [25662]=32525, + [25668]=24047, [25669]=14274, [25670]=32249, [25671]=35070, [25673]=21717, [25674]=15897, + [25675]=30530, [25676]=32311, [25679]=250, [25680]=31300, [25681]=37703, [25682]=31539, + [25683]=37986, [25685]=20771, [25686]=32862, [25687]=43976, [25689]=45010, [25690]=44808, + [25691]=33726, [25692]=51110, [25693]=36667, [25694]=25734, [25695]=24563, [25696]=50116, + [25697]=25155, [25699]=1250, [25700]=1250, [25701]=48531, [25702]=41752, [25703]=100, + [25707]=6250, [25708]=750, [25710]=26858, [25711]=28751, [25712]=21640, [25713]=34604, + [25714]=28836, [25715]=33525, [25716]=19266, [25717]=24843, [25718]=19949, [25719]=868, + [25720]=12500, [25721]=12500, [25722]=12500, [25723]=20, [25724]=22, [25725]=12500, + [25726]=2500, [25728]=15000, [25729]=15000, [25730]=15000, [25731]=15000, [25732]=30000, + [25733]=35000, [25734]=35000, [25735]=40000, [25736]=35000, [25737]=35000, [25738]=30000, + [25739]=40000, [25740]=40000, [25741]=30000, [25742]=30000, [25743]=30000, [25744]=676, + [25759]=79164, [25760]=99323, [25761]=99692, [25762]=100050, [25763]=80334, [25764]=80629, + [25772]=75090, [25773]=75369, [25774]=75656, [25775]=38775, [25776]=38775, [25777]=22952, + [25778]=13373, [25779]=16778, [25780]=20207, [25781]=43576, [25782]=37485, [25783]=18810, + [25784]=28546, [25785]=28546, [25786]=7464, [25787]=8910, [25788]=29291, [25789]=25060, + [25790]=31447, [25791]=21045, [25792]=25349, [25802]=1500, [25803]=41903, [25804]=41903, + [25805]=26207, [25806]=65751, [25808]=59918, [25809]=41903, [25810]=24153, [25811]=41903, + [25813]=1020, [25819]=56869, [25820]=36691, [25821]=40916, [25822]=32847, [25823]=69463, + [25824]=28695, [25825]=69960, [25826]=28695, [25835]=89596, [25836]=71940, [25838]=36230, + [25843]=8000, [25844]=32000, [25845]=40000, [25846]=10000, [25847]=30000, [25848]=25000, + [25849]=30000, [25861]=3, [25867]=30000, [25868]=30000, [25869]=20000, [25870]=20000, + [25872]=7, [25873]=18, [25875]=50, [25876]=200, [25878]=13253, [25880]=150, + [25881]=600, [25882]=1000, [25883]=1000, [25886]=250, [25887]=15000, [25890]=30000, + [25893]=30000, [25894]=30000, [25895]=30000, [25896]=30000, [25897]=30000, [25898]=30000, + [25899]=30000, [25900]=5500, [25901]=30000, [25902]=30000, [25903]=30000, [25904]=30000, + [25905]=30000, [25906]=30000, [25907]=30000, [25908]=30000, [25909]=30000, [25910]=30000, + [25913]=30284, [25914]=30284, [25915]=62470, [25916]=64487, [25917]=80897, [25918]=64948, + [25919]=30284, [25920]=63579, [25921]=30284, [25922]=21574, [25923]=27810, [25924]=23159, + [25925]=18598, [25926]=32023, [25927]=19247, [25928]=32023, [25929]=42788, [25930]=27607, + [25931]=18470, [25932]=30896, [25933]=65575, [25934]=82265, [25935]=82553, [25936]=66290, + [25937]=66290, [25939]=59195, [25940]=23761, [25941]=32513, [25942]=26119, [25943]=74482, + [25944]=93451, [25945]=22512, [25946]=28242, [25947]=34167, [25948]=22665, [25949]=16156, + [25950]=95526, [25951]=29422, [25952]=76979, [25953]=57944, [25954]=8377, [25955]=35014, + [25956]=27487, [25957]=23508, [25958]=21241, [25959]=18172, [25960]=15201, [25961]=12205, + [25962]=83797, [25963]=31154, [25964]=76819, [25965]=18583, [25966]=18652, [25971]=40426, + [25972]=40569, [25973]=40717, [25974]=17826, [25975]=22369, [25976]=26944, [25977]=31496, + [25978]=50177, [25979]=38850, [25980]=33423, [25981]=20968, [25982]=11225, [25983]=16901, + [25984]=21204, [25985]=75264, [25986]=75544, [25987]=75824, [25989]=28546, [25992]=28546, + [25993]=28546, [26004]=124, [26005]=323, [26006]=160, [26007]=384, [26008]=101, + [26009]=294, [26010]=183, [26011]=386, [26012]=148, [26013]=298, [26014]=93, + [26016]=169, [26017]=358, [26018]=152, [26019]=395, [26020]=191, [26021]=458, + [26022]=123, [26023]=360, [26024]=224, [26025]=473, [26026]=364, [26027]=181, + [26028]=126, [26030]=507, [26031]=196, [26032]=246, [26033]=589, [26034]=140, + [26035]=408, [26036]=255, [26037]=539, [26038]=424, [26039]=211, [26040]=147, + [26049]=325, [26050]=844, [26051]=409, [26052]=1063, [26053]=330, [26054]=797, + [26055]=92386, [26322]=113, [27388]=2000, [27389]=383, [27390]=996, [27398]=425, + [27399]=254, [27400]=608, [27401]=300, [27402]=718, [27403]=302, [27404]=671, + [27408]=39509, [27409]=28382, [27410]=22792, [27411]=22876, [27412]=95675, [27413]=18750, + [27414]=34701, [27415]=29022, [27416]=10307, [27417]=30024, [27418]=32140, [27420]=42412, + [27422]=8, [27423]=22786, [27424]=76249, [27425]=8, [27426]=76831, [27427]=53987, + [27428]=23226, [27429]=8, [27430]=46800, [27431]=78296, [27432]=36645, [27433]=23663, + [27434]=29689, [27435]=8, [27436]=4308, [27437]=8, [27438]=8, [27439]=8, + [27440]=12377, [27441]=8, [27442]=60000, [27443]=5000, [27445]=2500, [27446]=1, + [27447]=29989, [27448]=25656, [27449]=54936, [27450]=38946, [27451]=25946, [27452]=17361, + [27453]=7102, [27454]=40584, [27455]=47219, [27456]=45215, [27457]=31946, [27458]=54649, + [27459]=32175, [27460]=55035, [27461]=46024, [27462]=18474, [27463]=83894, [27464]=22464, + [27465]=16907, [27466]=25458, [27467]=31941, [27468]=21376, [27474]=26933, [27475]=31713, + [27476]=90421, [27477]=34450, [27478]=27321, [27481]=1, [27483]=20969, [27484]=25260, + [27485]=26057, [27487]=61250, [27488]=26347, [27489]=31029, [27490]=88468, [27491]=30866, + [27492]=44559, [27493]=17889, [27494]=26928, [27495]=41953, [27497]=31936, [27498]=125, + [27499]=112, [27500]=100, [27501]=100, [27502]=112, [27503]=125, [27505]=45504, + [27506]=34866, [27507]=65619, [27508]=17563, [27509]=22033, [27510]=26538, [27511]=25, + [27512]=89111, [27513]=20, [27514]=44876, [27515]=500, [27516]=500, [27517]=16449, + [27518]=24770, [27519]=24865, [27520]=43593, [27521]=25661, [27522]=25759, [27523]=14846, + [27524]=105570, [27525]=25432, [27526]=63818, [27527]=61238, [27528]=26340, [27529]=66290, + [27531]=22191, [27532]=12500, [27533]=91750, [27534]=22045, [27535]=29422, [27536]=16782, + [27537]=16847, [27538]=84562, [27539]=44476, [27540]=63903, [27541]=25659, [27542]=17171, + [27543]=86172, [27544]=25949, [27545]=43411, [27546]=8788, [27547]=17493, [27548]=30902, + [27549]=40882, [27550]=27229, [27551]=10283, [27552]=12, [27553]=30, [27631]=36689, + [27635]=6, [27636]=25, [27640]=606, [27641]=1014, [27651]=150, [27655]=150, + [27656]=150, [27657]=150, [27658]=150, [27659]=150, [27660]=150, [27661]=150, + [27662]=150, [27663]=150, [27664]=150, [27665]=150, [27666]=150, [27667]=150, + [27668]=5, [27669]=5, [27671]=200, [27672]=29874, [27673]=85196, [27674]=200, + [27676]=200, [27677]=200, [27678]=200, [27681]=200, [27682]=200, [27683]=10000, + [27684]=5000, [27685]=10, [27686]=10, [27687]=100, [27688]=5000, [27690]=7500, + [27691]=7500, [27692]=7500, [27693]=7500, [27694]=5000, [27695]=5000, [27696]=5000, + [27697]=7500, [27698]=7500, [27699]=7500, [27700]=7500, [27712]=21793, [27713]=39550, + [27714]=34450, [27715]=30889, [27716]=40188, [27717]=35626, [27721]=32499, [27722]=18673, + [27723]=15616, [27724]=23508, [27725]=32966, [27726]=17603, [27727]=22086, [27728]=17736, + [27729]=667, [27730]=17870, [27731]=17421, [27732]=17486, [27733]=30284, [27734]=30284, + [27735]=30284, [27736]=5000, [27737]=33294, [27738]=26730, [27739]=46860, [27740]=10709, + [27741]=90078, [27742]=16784, [27743]=25273, [27744]=25371, [27745]=25469, [27746]=17042, + [27747]=85539, [27748]=60105, [27749]=78430, [27750]=78718, [27751]=79015, [27752]=79311, + [27753]=60223, [27754]=60447, [27755]=31020, [27756]=60895, [27757]=110959, [27758]=22464, + [27759]=41287, [27760]=23018, [27761]=10709, [27762]=10709, [27763]=31588, [27764]=16912, + [27765]=21219, [27766]=39470, [27767]=85530, [27768]=17171, [27769]=107715, [27770]=21612, + [27771]=45495, [27772]=55769, [27773]=52479, [27775]=27134, [27776]=34036, [27778]=27424, + [27779]=50287, [27780]=83797, [27781]=25073, [27783]=20557, [27784]=41903, [27787]=41742, + [27788]=43913, [27789]=25236, [27790]=45438, [27791]=108780, [27792]=8788, [27793]=26298, + [27794]=65983, [27795]=18086, [27796]=27226, [27797]=34155, [27798]=32175, [27799]=36689, + [27800]=46024, [27801]=41753, [27802]=37916, [27803]=44126, [27804]=25360, [27805]=10709, + [27806]=26252, [27813]=47044, [27814]=90096, [27815]=27126, [27816]=27224, [27817]=68304, + [27818]=45694, [27821]=27707, [27822]=55035, [27823]=51920, [27824]=34743, [27825]=21793, + [27826]=39550, [27827]=21956, [27828]=10000, [27829]=110586, [27831]=33419, [27835]=27123, + [27837]=45527, [27838]=33962, [27839]=59661, [27840]=106935, [27842]=107748, [27843]=17304, + [27844]=45504, [27845]=39399, [27846]=87492, [27847]=46016, [27848]=26440, [27854]=280, + [27855]=280, [27856]=280, [27857]=280, [27858]=280, [27859]=280, [27860]=320, + [27865]=26147, [27866]=26242, [27867]=32925, [27868]=86036, [27869]=41903, [27870]=62137, + [27871]=8788, [27872]=91750, [27873]=46037, [27874]=50151, [27875]=33564, [27876]=84237, + [27877]=105703, [27878]=25463, [27884]=44417, [27885]=63806, [27886]=25617, [27887]=56311, + [27888]=39736, [27889]=17722, [27890]=66698, [27891]=66290, [27892]=26870, [27893]=62913, + [27895]=30866, [27896]=10000, [27897]=57778, [27898]=62144, [27899]=85196, [27900]=21612, + [27901]=83803, [27902]=25849, [27903]=111025, [27904]=10709, [27905]=89462, [27906]=62851, + [27907]=36045, [27908]=45215, [27909]=54453, [27910]=58292, [27911]=22851, [27912]=55034, + [27913]=92049, [27914]=33819, [27915]=37017, [27916]=43988, [27917]=24759, [27918]=29163, + [27919]=25630, [27925]=7102, [27932]=870, [27933]=990, [27934]=1185, [27935]=1325, + [27936]=51920, [27937]=86858, [27938]=32690, [27940]=100000, [27941]=50000, [27944]=87500, + [27945]=37500, [27946]=26928, [27948]=36165, [27976]=62500, [27977]=60782, [27978]=2500, + [27979]=25000, [27980]=87800, [27981]=26437, [27982]=75000, [27985]=32296, [27986]=115094, + [27987]=61203, [27988]=24576, [27992]=12500, [27993]=37576, [27994]=25143, [27995]=31548, + [27996]=30866, [28026]=19557, [28027]=22457, [28028]=14852, [28029]=14047, [28030]=21753, + [28031]=22437, [28032]=22518, [28033]=107191, [28034]=10000, [28040]=66290, [28041]=66290, + [28042]=66290, [28050]=33134, [28051]=33255, [28052]=26703, [28053]=2, [28055]=47247, + [28056]=3, [28057]=40796, [28058]=870, [28059]=990, [28060]=2, [28061]=3, + [28062]=46848, [28063]=43673, [28064]=17536, [28065]=17603, [28066]=17669, [28068]=15000, + [28069]=36740, [28070]=21111, [28071]=3750, [28072]=6500, [28073]=16750, [28074]=26782, + [28075]=21505, [28100]=1000, [28101]=1500, [28102]=2500, [28103]=2500, [28104]=2500, + [28108]=10307, [28109]=10307, [28121]=10000, [28124]=21217, [28134]=22464, [28141]=134, + [28142]=169, [28143]=203, [28144]=77, [28146]=48, [28147]=60, [28148]=72, + [28149]=158, [28150]=149, [28151]=449, [28152]=240, [28153]=257, [28154]=334, + [28155]=193, [28156]=193, [28157]=422, [28158]=243, [28159]=244, [28160]=476, + [28161]=478, [28162]=276, [28163]=277, [28164]=876, [28166]=46065, [28167]=31029, + [28168]=39818, [28169]=24112, [28170]=26735, [28171]=22359, [28172]=40625, [28173]=48927, + [28174]=18082, [28175]=57488, [28176]=44320, [28177]=38377, [28178]=31961, [28179]=25664, + [28180]=44997, [28181]=38789, [28182]=32446, [28183]=26052, [28184]=85100, [28185]=34167, + [28186]=51441, [28187]=22045, [28188]=110575, [28189]=88785, [28190]=10000, [28191]=35771, + [28192]=41438, [28193]=25081, [28194]=25176, [28202]=43253, [28203]=60782, [28204]=43578, + [28205]=61232, [28206]=32925, [28207]=46177, [28210]=91424, [28211]=55035, [28212]=36830, + [28213]=34450, [28214]=20977, [28215]=37906, [28216]=84562, [28217]=41953, [28218]=34081, + [28219]=51318, [28220]=32192, [28221]=45154, [28222]=108122, [28223]=10000, [28224]=33551, + [28225]=47053, [28226]=90122, [28227]=83797, [28228]=54458, [28229]=36436, [28230]=36566, + [28231]=55044, [28232]=36823, [28233]=22464, [28248]=27226, [28249]=27324, [28250]=27422, + [28251]=34396, [28252]=36819, [28253]=115468, [28254]=22464, [28255]=31579, [28256]=26060, + [28257]=87193, [28258]=43988, [28259]=30866, [28260]=22045, [28262]=62162, [28263]=89128, + [28264]=44727, [28265]=55035, [28266]=54057, [28267]=90421, [28268]=22686, [28269]=27319, + [28270]=15000, [28271]=20000, [28272]=20000, [28273]=15000, [28274]=10000, [28275]=38940, + [28276]=15000, [28277]=15000, [28278]=26250, [28279]=30000, [28280]=30000, [28281]=15000, + [28282]=10000, [28284]=75, [28285]=47035, [28286]=67565, [28288]=10000, [28290]=2500, + [28291]=15000, [28296]=26052, [28301]=26538, [28303]=403, [28304]=17885, [28306]=37788, + [28311]=85213, [28315]=86506, [28316]=55572, [28317]=17431, [28318]=45836, [28321]=8788, + [28322]=91099, [28323]=7102, [28324]=32296, [28325]=115094, [28327]=14846, [28328]=25271, + [28337]=62630, [28338]=35918, [28339]=33796, [28340]=33918, [28341]=113456, [28342]=36436, + [28343]=10283, [28344]=41467, [28345]=92058, [28347]=41951, [28348]=31582, [28349]=38045, + [28350]=44472, [28367]=104856, [28370]=10000, [28371]=26252, [28372]=26350, [28373]=26448, + [28374]=26545, [28375]=31258, [28384]=41455, [28386]=64411, [28387]=22045, [28390]=30685, + [28391]=52500, [28392]=87826, [28393]=110190, [28394]=41903, [28396]=22279, [28397]=67077, + [28398]=22440, [28399]=280, [28400]=90413, [28401]=50552, [28403]=59434, [28406]=25762, + [28407]=55035, [28408]=9, [28412]=22045, [28413]=26440, [28414]=33172, [28415]=26635, + [28416]=89102, [28418]=10000, [28419]=39470, [28420]=750, [28421]=1000, [28425]=106842, + [28426]=121723, [28427]=133976, [28428]=135064, [28429]=153869, [28430]=169367, [28431]=109260, + [28432]=124467, [28433]=140623, [28434]=141739, [28435]=161430, [28436]=177659, [28437]=114600, + [28438]=130528, [28439]=129941, [28440]=131012, [28441]=149270, [28442]=164324, [28452]=175, + [28453]=30149, [28454]=36305, [28458]=5000, [28459]=5000, [28460]=5000, [28461]=5000, + [28462]=5000, [28463]=5000, [28464]=5000, [28465]=5000, [28466]=5000, [28467]=5000, + [28468]=5000, [28469]=5000, [28470]=5000, [28477]=24459, [28483]=76235, [28484]=89420, + [28485]=102060, [28486]=280, [28491]=22464, [28492]=28179, [28493]=34088, [28494]=39661, + [28495]=13258, [28496]=16632, [28497]=18615, [28498]=21925, [28501]=25, [28502]=40765, + [28503]=34870, [28504]=87501, [28505]=41219, [28506]=29384, [28507]=23591, [28508]=23678, + [28509]=128862, [28510]=60256, [28511]=23936, [28512]=42281, [28514]=28084, [28515]=22554, + [28516]=86443, [28517]=34092, [28518]=40150, [28519]=34349, [28520]=34479, [28521]=28841, + [28522]=115788, [28523]=34866, [28524]=116656, [28525]=203059, [28528]=91160, [28529]=35640, + [28530]=88355, [28531]=26809, [28532]=27678, [28533]=28546, [28534]=29415, [28535]=30284, + [28536]=31154, [28537]=32023, [28538]=32891, [28539]=33760, [28540]=34629, [28541]=35498, + [28542]=36368, [28543]=37236, [28544]=38105, [28545]=43900, [28565]=24032, [28566]=42450, + [28567]=36305, [28568]=36436, [28569]=63869, [28570]=36696, [28572]=111436, [28573]=139838, + [28578]=46850, [28579]=91160, [28581]=88820, [28582]=35654, [28583]=53677, [28584]=119717, + [28585]=36045, [28586]=36172, [28587]=151260, [28588]=91081, [28589]=55082, [28590]=91160, + [28591]=61366, [28592]=36949, [28593]=58607, [28594]=46155, [28595]=2500, [28596]=12500, + [28597]=81675, [28599]=70528, [28600]=58990, [28601]=59202, [28602]=47535, [28603]=203059, + [28604]=149618, [28606]=77160, [28608]=63623, [28609]=102777, [28610]=51371, [28611]=73015, + [28612]=34352, [28621]=82874, [28631]=51556, [28632]=15000, [28633]=143665, [28647]=42122, + [28649]=60256, [28652]=22897, [28653]=34472, [28654]=23068, [28655]=28944, [28656]=34863, + [28657]=116632, [28658]=150219, [28659]=36689, [28660]=36313, [28661]=60256, [28662]=85329, + [28663]=36700, [28666]=58626, [28669]=42439, [28670]=31250, [28671]=51318, [28672]=34338, + [28673]=86172, [28674]=88355, [28675]=60256, [28726]=34483, [28727]=91160, [28728]=203059, + [28729]=116234, [28730]=60256, [28731]=102777, [28732]=44071, [28733]=41521, [28734]=203059, + [28735]=71295, [28740]=67403, [28741]=56386, [28742]=45283, [28743]=59548, [28744]=34219, + [28745]=102777, [28746]=51949, [28747]=60451, [28748]=81051, [28749]=116222, [28750]=29164, + [28751]=70247, [28752]=44067, [28753]=60256, [28754]=75763, [28755]=45716, [28756]=36703, + [28757]=113642, [28762]=102777, [28763]=60256, [28764]=36919, [28765]=37059, [28766]=37196, + [28767]=124456, [28768]=124925, [28770]=125848, [28771]=129671, [28772]=97604, [28773]=163260, + [28774]=163829, [28775]=68922, [28776]=33000, [28777]=39740, [28778]=36076, [28779]=42494, + [28780]=24238, [28781]=203059, [28782]=152643, [28783]=91937, [28785]=91160, [28788]=2500, + [28789]=91160, [28790]=60256, [28791]=60256, [28792]=60256, [28793]=60256, [28794]=163813, + [28795]=46294, [28796]=49495, [28797]=39732, [28799]=24142, [28800]=151457, [28801]=54735, + [28802]=122102, [28803]=47221, [28804]=37914, [28810]=58388, [28822]=102777, [28823]=91160, + [28824]=44481, [28825]=81175, [28826]=36689, [28827]=38328, [28828]=32057, [28830]=91160, + [28972]=2350, [28979]=3, [29007]=7, [29008]=18, [29009]=50, [29010]=200, + [29013]=200, [29014]=200, [29108]=58074, [29109]=72850, [29110]=1, [29111]=1, + [29112]=75, [29115]=68812, [29116]=46037, [29117]=33434, [29118]=25000, [29119]=197528, + [29121]=399034, [29122]=125555, [29123]=197528, [29124]=414544, [29125]=416074, [29126]=197528, + [29127]=62408, [29128]=7102, [29129]=35918, [29130]=112653, [29131]=45224, [29132]=43988, + [29133]=113863, [29134]=32178, [29135]=41279, [29136]=41426, [29137]=104471, [29138]=104878, + [29139]=25265, [29140]=26062, [29141]=43600, [29142]=43763, [29143]=25000, [29144]=25000, + [29145]=43988, [29146]=43988, [29147]=33426, [29148]=33548, [29151]=318849, [29152]=319996, + [29153]=428192, [29155]=431211, [29156]=402301, [29157]=1500, [29158]=1750, [29159]=8375, + [29160]=9125, [29165]=415991, [29166]=111003, [29167]=111410, [29168]=43988, [29169]=43988, + [29170]=197528, [29171]=531363, [29172]=197528, [29173]=43988, [29174]=25474, [29175]=400730, + [29176]=268985, [29177]=197528, [29179]=43988, [29180]=26057, [29181]=197528, [29182]=411359, + [29183]=86279, [29184]=61700, [29185]=88468, [29186]=250000, [29187]=75000, [29189]=250000, + [29190]=250000, [29191]=250000, [29192]=250000, [29193]=250000, [29194]=250000, [29195]=250000, + [29196]=250000, [29197]=250000, [29198]=250000, [29199]=250000, [29200]=3658, [29201]=150, + [29202]=600, [29203]=10000, [29204]=20000, [29209]=500, [29211]=9954, [29212]=30000, + [29213]=30000, [29214]=40000, [29215]=40000, [29217]=30000, [29218]=30000, [29219]=30000, + [29232]=20000, [29238]=39896, [29239]=59610, [29240]=22835, [29241]=22916, [29242]=34499, + [29243]=34624, [29244]=34749, [29245]=52539, [29246]=29163, [29247]=29268, [29248]=44054, + [29249]=23578, [29250]=21406, [29251]=32235, [29252]=37965, [29253]=39163, [29254]=58518, + [29255]=22418, [29257]=22582, [29258]=33999, [29259]=34124, [29261]=34371, [29262]=51974, + [29263]=28848, [29264]=28952, [29265]=43585, [29292]=10, [29312]=11665, [29313]=33669, + [29314]=37384, [29315]=15009, [29316]=41649, [29317]=16377, [29318]=30818, [29319]=24743, + [29320]=7102, [29321]=7102, [29322]=7102, [29323]=7102, [29325]=37866, [29326]=32789, + [29327]=18203, [29328]=14618, [29329]=106924, [29330]=34450, [29332]=32440, [29333]=41953, + [29334]=41953, [29335]=41953, [29336]=41953, [29337]=55814, [29339]=48180, [29340]=40294, + [29341]=32350, [29342]=55280, [29343]=35848, [29344]=43186, [29345]=28902, [29346]=108284, + [29347]=88355, [29348]=104629, [29349]=88355, [29350]=84694, [29351]=81510, [29352]=60256, + [29353]=109480, [29354]=34374, [29355]=137836, [29356]=138335, [29357]=28958, [29359]=139821, + [29360]=112256, [29362]=113045, [29363]=100000, [29364]=25000, [29371]=69666, [29372]=69923, + [29377]=88988, [29378]=53585, [29380]=71952, [29391]=69653, [29393]=280, [29394]=400, + [29395]=320, [29398]=30284, [29399]=72796, [29400]=17538, [29401]=320, [29412]=280, + [29425]=200, [29426]=200, [29448]=400, [29449]=400, [29450]=400, [29451]=400, + [29452]=400, [29453]=400, [29454]=280, [29456]=69716, [29457]=69987, [29458]=77562, + [29463]=39750, [29483]=15000, [29485]=15000, [29486]=15000, [29487]=15000, [29488]=15000, + [29489]=52781, [29490]=26486, [29491]=40049, [29492]=53353, [29493]=40332, [29494]=24968, + [29495]=41772, [29496]=20965, [29497]=31564, [29498]=42245, [29499]=31803, [29500]=21281, + [29502]=40447, [29503]=27064, [29504]=40743, [29505]=40893, [29506]=27361, [29507]=27461, + [29508]=49606, [29509]=33190, [29510]=46388, [29511]=31045, [29512]=46950, [29514]=62803, + [29515]=71156, [29516]=34352, [29517]=31758, [29518]=936, [29519]=72225, [29520]=34870, + [29521]=35000, [29522]=60864, [29523]=29381, [29524]=29489, [29525]=61534, [29526]=30483, + [29527]=30589, [29528]=5000, [29529]=5000, [29530]=5000, [29531]=5000, [29532]=5000, + [29533]=20000, [29534]=20000, [29535]=40000, [29536]=40000, [29539]=1250, [29540]=30000, + [29547]=1250, [29548]=2000, [29549]=100000, [29550]=10000, [29553]=870, [29554]=990, + [29555]=1325, [29556]=1185, [29559]=1185, [29560]=1325, [29561]=870, [29562]=990, + [29563]=1185, [29564]=1325, [29567]=1185, [29568]=1325, [29570]=670, [29571]=670, + [29572]=670, [29573]=55, [29574]=80, [29575]=670, [29576]=670, [29577]=87, + [29578]=990, [29579]=870, [29580]=1185, [29581]=1325, [29583]=1462, [29584]=150, + [29664]=12500, [29669]=12500, [29672]=12500, [29673]=12500, [29674]=12500, [29675]=12500, + [29677]=20000, [29682]=20000, [29684]=20000, [29689]=20000, [29691]=20000, [29693]=20000, + [29698]=20000, [29700]=20000, [29701]=20000, [29702]=20000, [29703]=20000, [29704]=20000, + [29713]=30000, [29714]=40000, [29717]=30000, [29718]=40000, [29719]=15000, [29720]=15000, + [29721]=30000, [29722]=30000, [29723]=60000, [29724]=60000, [29725]=60000, [29726]=60000, + [29727]=60000, [29728]=60000, [29729]=60000, [29730]=60000, [29731]=60000, [29732]=60000, + [29733]=60000, [29734]=60000, [29753]=50000, [29754]=50000, [29755]=50000, [29756]=50000, + [29757]=50000, [29758]=50000, [29759]=50000, [29760]=50000, [29761]=50000, [29762]=50000, + [29763]=50000, [29764]=50000, [29765]=50000, [29766]=50000, [29767]=50000, [29771]=14706, + [29772]=18446, [29773]=33322, [29774]=52018, [29775]=34629, [29776]=34629, [29777]=19896, + [29779]=50120, [29780]=32469, [29781]=39728, [29782]=35891, [29783]=57552, [29784]=17922, + [29785]=21586, [29786]=37841, [29787]=72472, [29788]=43641, [29789]=51098, [29791]=38522, + [29792]=22133, [29793]=35498, [29794]=35498, [29799]=1185, [29800]=1325, [29804]=26881, + [29806]=21660, [29807]=25507, [29808]=21818, [29810]=27467, [29811]=22052, [29812]=25967, + [29813]=25562, [29814]=27999, [29815]=27999, [29901]=1250, [29902]=25000, [29903]=1250, + [29904]=1250, [29908]=63676, [29909]=51139, [29910]=64172, [29911]=64414, [29913]=53349, + [29914]=53547, [29915]=40305, [29916]=40454, [29917]=34647, [29918]=26216, [29919]=34898, + [29920]=60256, [29921]=79504, [29922]=60256, [29923]=203059, [29924]=139688, [29925]=40322, + [29926]=22362, [29927]=16831, [29928]=20942, [29929]=26277, [29930]=26374, [29931]=19855, + [29932]=31887, [29933]=32006, [29934]=24091, [29935]=37614, [29936]=37753, [29937]=28362, + [29938]=27162, [29939]=450000, [29940]=162500, [29941]=250000, [29942]=33067, [29943]=24890, + [29944]=36094, [29945]=36229, [29946]=27224, [29947]=31454, [29948]=131728, [29949]=99160, + [29950]=10000, [29951]=10000, [29952]=10000, [29953]=10000, [29954]=10000, [29955]=10000, + [29956]=10000, [29957]=10000, [29958]=10000, [29959]=10000, [29960]=10000, [29962]=128733, + [29964]=15290, [29965]=43947, [29966]=31332, [29967]=33680, [29968]=40565, [29969]=35561, + [29970]=15642, [29971]=18839, [29972]=51269, [29973]=38564, [29974]=38708, [29975]=46617, + [29976]=40050, [29977]=53592, [29978]=30242, [29979]=28450, [29980]=2500, [29981]=160275, + [29982]=96539, [29983]=64912, [29984]=31089, [29985]=74902, [29986]=37590, [29987]=26958, + [29988]=172526, [29989]=40745, [29990]=40895, [29991]=82099, [29992]=42305, [29993]=180462, + [29994]=42609, [29995]=71271, [29996]=145925, [29997]=60256, [29998]=50711, [29999]=27088, + [30000]=0, [30001]=19742, [30002]=34618, [30003]=17412, [30004]=31603, [30005]=36774, + [30006]=36368, [30007]=86443, [30008]=88355, [30009]=105395, [30010]=105783, [30011]=106170, + [30012]=106558, [30013]=85548, [30014]=41860, [30015]=102777, [30016]=44182, [30017]=88355, + [30018]=102777, [30019]=51135, [30020]=27073, [30021]=160225, [30022]=102777, [30023]=37156, + [30024]=38324, [30025]=36368, [30026]=38611, [30027]=67691, [30028]=60256, [30029]=32531, + [30030]=39181, [30031]=68688, [30032]=46305, [30033]=69183, [30034]=46642, [30035]=39891, + [30036]=26690, [30037]=40179, [30038]=26881, [30039]=50577, [30040]=31458, [30041]=47366, + [30042]=31697, [30043]=57519, [30044]=38320, [30045]=57952, [30046]=38607, [30047]=38747, + [30048]=67929, [30049]=203059, [30050]=33564, [30051]=39317, [30052]=60256, [30053]=69176, + [30054]=79488, [30055]=49859, [30056]=49567, [30057]=43787, [30058]=130230, [30059]=88355, + [30060]=47182, [30061]=60256, [30062]=31691, [30063]=38172, [30064]=25544, [30065]=89730, + [30066]=58156, [30067]=33564, [30068]=38886, [30069]=22391, [30070]=19263, [30071]=32222, + [30072]=32340, [30073]=33297, [30074]=51597, [30075]=61713, [30076]=44570, [30077]=74563, + [30079]=37598, [30080]=98401, [30081]=66166, [30082]=132200, [30083]=60256, [30084]=66912, + [30085]=57934, [30086]=96412, [30087]=77416, [30088]=97120, [30089]=80037, [30090]=174677, + [30091]=40338, [30092]=50597, [30093]=101472, [30095]=128719, [30096]=43605, [30097]=56209, + [30098]=37451, [30099]=86443, [30100]=37734, [30101]=63129, [30102]=95073, [30103]=139053, + [30104]=61847, [30105]=107885, [30106]=35382, [30107]=56812, [30108]=145402, [30109]=60256, + [30110]=60256, [30111]=54025, [30112]=50892, [30155]=150, [30183]=20000, [30218]=26421, + [30224]=21594, [30225]=25427, [30226]=54369, [30227]=34629, [30236]=50000, [30237]=50000, + [30238]=50000, [30239]=50000, [30240]=50000, [30241]=50000, [30242]=50000, [30243]=50000, + [30244]=50000, [30245]=50000, [30246]=50000, [30247]=50000, [30248]=50000, [30249]=50000, + [30250]=50000, [30252]=51684, [30253]=14527, [30254]=25661, [30255]=43902, [30256]=33847, + [30257]=42459, [30258]=59660, [30262]=26795, [30263]=32412, [30264]=25332, [30265]=13760, + [30266]=25899, [30267]=24403, [30268]=27154, [30269]=25551, [30270]=47874, [30271]=21111, + [30272]=35317, [30273]=32042, [30274]=20823, [30275]=36505, [30276]=34629, [30277]=71933, + [30278]=72196, [30279]=54344, [30280]=30000, [30281]=30000, [30282]=30000, [30283]=30000, + [30284]=20559, [30285]=17198, [30286]=31214, [30290]=35050, [30291]=36867, [30293]=21612, + [30294]=20821, [30295]=31483, [30296]=48936, [30297]=31838, [30298]=52460, [30299]=61414, + [30300]=41903, [30301]=60000, [30302]=60000, [30303]=60000, [30304]=60000, [30305]=60000, + [30306]=60000, [30307]=60000, [30308]=60000, [30321]=60000, [30322]=60000, [30323]=60000, + [30324]=60000, [30328]=35632, [30329]=32187, [30330]=25950, [30331]=14798, [30332]=18565, + [30333]=33684, [30334]=39191, [30335]=28148, [30336]=22599, [30337]=39610, [30338]=22758, + [30339]=36368, [30340]=36368, [30341]=17345, [30342]=20895, [30352]=26134, [30355]=400, + [30357]=400, [30358]=400, [30359]=400, [30361]=400, [30362]=25556, [30363]=41039, + [30364]=68655, [30365]=7086, [30366]=7086, [30368]=25292, [30369]=31731, [30370]=25475, + [30371]=30000, [30372]=21384, [30373]=32193, [30374]=38940, [30375]=30434, [30377]=7086, + [30378]=7086, [30379]=35377, [30380]=25000, [30381]=37358, [30382]=14311, [30383]=13678, + [30384]=17162, [30386]=36236, [30394]=86762, [30395]=87099, [30396]=87435, [30397]=50141, + [30398]=20638, [30399]=17264, [30400]=24400, [30401]=25452, [30402]=23981, [30405]=120, + [30419]=750, [30420]=677, [30421]=2542, [30422]=8375, [30443]=20000, [30444]=12500, + [30446]=72039, [30447]=72039, [30448]=72039, [30449]=72039, [30450]=72039, [30457]=320, + [30458]=280, [30463]=17128, [30466]=1185, [30467]=1325, [30475]=1185, [30476]=1325, + [30477]=1185, [30478]=1325, [30483]=10000, [30504]=1258, [30505]=631, [30506]=375, + [30507]=82, [30508]=27, [30509]=55, [30510]=1185, [30511]=1325, [30512]=1185, + [30513]=1325, [30514]=20361, [30515]=20438, [30516]=13677, [30517]=28831, [30518]=28939, + [30519]=21785, [30520]=14229, [30521]=13930, [30522]=91772, [30523]=55355, [30531]=43665, + [30532]=43827, [30533]=76989, [30534]=66241, [30535]=55409, [30536]=77857, [30538]=56029, + [30541]=69770, [30542]=5000, [30543]=46846, [30544]=5000, [30546]=30000, [30547]=30000, + [30548]=30000, [30549]=30000, [30550]=30000, [30551]=30000, [30552]=30000, [30553]=30000, + [30554]=30000, [30555]=30000, [30556]=30000, [30558]=30000, [30559]=30000, [30560]=30000, + [30563]=30000, [30564]=30000, [30565]=30000, [30566]=30000, [30568]=5535, [30569]=3633, + [30570]=17545, [30571]=35243, [30572]=30000, [30573]=30000, [30574]=30000, [30575]=30000, + [30581]=30000, [30582]=30000, [30583]=30000, [30584]=30000, [30585]=30000, [30586]=30000, + [30587]=30000, [30588]=30000, [30589]=30000, [30590]=30000, [30591]=30000, [30592]=30000, + [30593]=30000, [30594]=30000, [30597]=18492, [30598]=35243, [30599]=5535, [30600]=30000, + [30601]=30000, [30602]=30000, [30603]=30000, [30604]=30000, [30605]=30000, [30606]=30000, + [30607]=30000, [30608]=30000, [30610]=280, [30611]=6, [30612]=6, [30615]=1333, + [30619]=91160, [30620]=72039, [30621]=72039, [30622]=25000, [30623]=25000, [30626]=91160, + [30627]=91160, [30629]=91160, [30633]=25000, [30634]=25000, [30635]=25000, [30637]=25000, + [30641]=58878, [30642]=33835, [30643]=33965, [30644]=28413, [30663]=72039, [30664]=72039, + [30665]=72039, [30666]=88355, [30667]=60256, [30668]=23692, [30673]=24124, [30674]=45395, + [30675]=24295, [30676]=30477, [30677]=36703, [30678]=43218, [30681]=42122, [30682]=50968, + [30683]=60937, [30684]=93381, [30685]=29290, [30686]=35278, [30687]=41542, [30696]=15000, + [30705]=46039, [30707]=31466, [30708]=26648, [30709]=33564, [30710]=50287, [30720]=72039, + [30722]=150514, [30723]=120862, [30724]=90976, [30725]=24350, [30726]=102777, [30727]=49062, + [30728]=55392, [30729]=37063, [30730]=61998, [30731]=65210, [30732]=144890, [30733]=116363, + [30734]=46725, [30735]=35179, [30736]=60256, [30737]=44308, [30738]=60256, [30739]=71427, + [30740]=62616, [30741]=42221, [30744]=30000, [30745]=35000, [30746]=25000, [30747]=30000, + [30748]=35000, [30749]=29108, [30750]=37874, [30751]=29676, [30752]=33679, [30753]=65333, + [30754]=27483, [30755]=50995, [30757]=20851, [30758]=20675, [30759]=20499, [30765]=25321, + [30771]=13006, [30775]=21719, [30777]=11851, [30781]=20200, [30784]=10100, [30787]=90791, + [30788]=91108, [30789]=114292, [30804]=1064, [30809]=200, [30810]=200, [30811]=175, + [30812]=1185, [30813]=1325, [30814]=1185, [30815]=1325, [30816]=5, [30817]=150, + [30818]=9, [30819]=60, [30820]=1325, [30821]=1185, [30823]=75000, [30825]=37037, + [30826]=30000, [30830]=501016, [30831]=25066, [30832]=100968, [30833]=15000, [30834]=49382, + [30835]=52125, [30836]=34880, [30837]=17088, [30838]=17430, [30839]=35267, [30841]=43988, + [30842]=15000, [30843]=20000, [30844]=25000, [30846]=250000, [30855]=54657, [30856]=28744, + [30857]=27388, [30859]=62985, [30860]=41555, [30861]=51559, [30862]=51744, [30863]=36880, + [30864]=44409, [30865]=134401, [30866]=70700, [30868]=33989, [30869]=40943, [30870]=27400, + [30871]=27502, [30872]=203059, [30873]=62628, [30874]=139081, [30878]=75931, [30879]=36354, + [30880]=65964, [30881]=146462, [30882]=94071, [30883]=184368, [30884]=44405, [30885]=40316, + [30886]=50586, [30887]=81251, [30888]=27188, [30889]=87338, [30891]=51561, [30892]=64079, + [30893]=85374, [30894]=37542, [30895]=28664, [30896]=100692, [30897]=50818, [30898]=72439, + [30899]=72701, [30900]=87555, [30901]=146434, [30902]=195738, [30903]=110003, [30904]=110394, + [30905]=71583, [30906]=107792, [30907]=86568, [30908]=186039, [30909]=95599, [30910]=149931, + [30911]=203059, [30912]=60418, [30913]=60635, [30914]=35696, [30915]=50444, [30916]=57527, + [30917]=54128, [30918]=144864, [30919]=43616, [30922]=22675, [30923]=15170, [30924]=15901, + [30925]=24553, [30926]=20879, [30927]=14314, [30928]=28046, [30929]=28151, [30930]=14474, + [30931]=21789, [30932]=16799, [30933]=42153, [30936]=18492, [30937]=18559, [30938]=27273, + [30939]=32306, [30940]=17421, [30941]=34142, [30942]=17556, [30943]=20303, [30944]=20381, + [30945]=35512, [30946]=26096, [30947]=41907, [30948]=49654, [30950]=21188, [30951]=25103, + [30952]=32948, [30953]=33069, [30955]=32370, [30956]=22189, [30957]=45695, [30958]=31251, + [30959]=48587, [30960]=47610, [30961]=36652, [30962]=28475, [30964]=37072, [30966]=24490, + [30967]=29018, [30968]=37632, [30971]=21785, [30973]=40860, [30981]=36368, [30984]=52160, + [30986]=44524, [30999]=79950, [31000]=60195, [31002]=80880, [31009]=106568, [31010]=106956, + [31013]=86486, [31025]=24577, [31031]=25116, [31033]=25295, [31036]=108344, [31038]=101456, + [31062]=103834, [31071]=79665, [31072]=59981, [31073]=80285, [31074]=41555, [31075]=41555, + [31076]=41555, [31077]=41555, [31078]=41555, [31079]=10000, [31080]=4000, [31089]=50000, + [31090]=50000, [31091]=50000, [31092]=50000, [31093]=50000, [31094]=50000, [31095]=50000, + [31096]=50000, [31097]=50000, [31098]=50000, [31099]=50000, [31100]=50000, [31101]=50000, + [31102]=50000, [31103]=50000, [31104]=25765, [31105]=45161, [31106]=38922, [31107]=39062, + [31109]=32781, [31110]=32897, [31111]=13825, [31112]=26020, [31113]=43988, [31114]=20977, + [31115]=36782, [31116]=30000, [31117]=30000, [31118]=30000, [31125]=33546, [31126]=17317, + [31127]=41706, [31131]=16005, [31133]=25608, [31134]=82877, [31136]=48207, [31137]=24331, + [31138]=19802, [31139]=69628, [31140]=20965, [31142]=66009, [31143]=21832, [31145]=25499, + [31147]=28836, [31148]=27782, [31149]=13599, [31150]=13599, [31151]=24635, [31152]=50352, + [31153]=72203, [31154]=750, [31155]=58153, [31156]=50031, [31157]=41843, [31158]=33599, + [31159]=44177, [31160]=38247, [31161]=31844, [31162]=25568, [31163]=28611, [31164]=22786, + [31165]=18988, [31166]=15259, [31168]=43954, [31170]=37218, [31172]=31015, [31173]=31634, + [31174]=24812, [31175]=17496, [31176]=24964, [31177]=20399, [31178]=38775, [31179]=17928, + [31180]=25335, [31181]=13599, [31182]=33546, [31183]=31627, [31184]=24007, [31185]=19206, + [31186]=90367, [31187]=32533, [31188]=24635, [31189]=20996, [31190]=26991, [31191]=17496, + [31192]=13997, [31193]=71976, [31194]=37731, [31195]=27778, [31196]=8788, [31197]=22786, + [31198]=18988, [31199]=15191, [31200]=47338, [31201]=24836, [31202]=26036, [31203]=39818, + [31204]=55282, [31209]=56839, [31210]=48900, [31211]=40905, [31212]=32848, [31213]=41885, + [31214]=38398, [31215]=31973, [31216]=25672, [31217]=45002, [31218]=35970, [31219]=29975, + [31220]=23980, [31221]=26736, [31222]=36400, [31223]=22786, [31224]=18921, [31225]=15191, + [31226]=36982, [31227]=24126, [31228]=20399, [31229]=16999, [31230]=23239, [31231]=13599, + [31232]=35631, [31233]=30735, [31234]=80717, [31235]=25499, [31236]=21834, [31237]=40796, + [31238]=36689, [31240]=49481, [31242]=50527, [31243]=39605, [31244]=33004, [31246]=26403, + [31247]=35821, [31248]=31012, [31249]=24753, [31250]=20737, [31254]=21593, [31255]=24298, + [31256]=658, [31258]=55035, [31263]=523, [31264]=420, [31268]=1066, [31269]=1070, + [31270]=805, [31272]=24033, [31275]=41903, [31276]=40842, [31277]=41903, [31280]=23383, + [31281]=29587, [31282]=31177, [31283]=15899, [31284]=28806, [31285]=41068, [31286]=47960, + [31287]=51157, [31288]=29975, [31289]=99916, [31290]=10709, [31291]=107599, [31292]=52430, + [31293]=24576, [31294]=40363, [31295]=40961, [31297]=32769, [31298]=58768, [31299]=105351, + [31303]=64180, [31304]=85891, [31305]=86216, [31306]=33564, [31308]=104889, [31312]=21446, + [31313]=17936, [31314]=32544, [31315]=25434, [31318]=123328, [31319]=79025, [31320]=69592, + [31321]=115200, [31322]=125228, [31323]=75416, [31326]=60256, [31328]=63118, [31329]=28260, + [31330]=47683, [31331]=106334, [31332]=106717, [31333]=40162, [31334]=134340, [31335]=53927, + [31336]=432949, [31337]=6250, [31338]=88355, [31339]=60256, [31340]=39762, [31341]=20052, + [31342]=102901, [31343]=41313, [31354]=10000, [31355]=10000, [31356]=10000, [31357]=10000, + [31358]=30000, [31359]=15000, [31361]=15000, [31362]=30000, [31364]=82308, [31367]=83219, + [31368]=62517, [31369]=83818, [31370]=74152, [31371]=55708, [31380]=41903, [31381]=41903, + [31382]=41903, [31383]=41903, [31390]=60000, [31391]=60000, [31392]=60000, [31393]=60000, + [31394]=60000, [31395]=60000, [31398]=60256, [31399]=60256, [31401]=30000, [31402]=30000, + [31414]=83568, [31415]=44029, [31416]=50525, [31417]=102273, [31418]=12893, [31419]=24266, + [31420]=29359, [31421]=22947, [31422]=65433, [31423]=67420, [31424]=50747, [31425]=13580, + [31426]=17036, [31427]=30911, [31428]=24155, [31429]=20660, [31430]=15631, [31431]=18830, + [31432]=33015, [31433]=25968, [31434]=32585, [31435]=30196, [31436]=35293, [31438]=19848, + [31439]=17050, [31440]=20533, [31441]=35996, [31442]=27578, [31443]=34597, [31444]=31247, + [31445]=24530, [31446]=69937, [31447]=70180, [31448]=70430, [31452]=12932, [31453]=24342, + [31454]=19549, [31455]=23653, [31456]=20754, [31457]=26039, [31458]=41816, [31459]=48960, + [31460]=31388, [31461]=17899, [31462]=26944, [31464]=22616, [31465]=27237, [31470]=25002, + [31471]=32246, [31472]=19420, [31473]=22872, [31474]=50204, [31475]=67189, [31476]=67439, + [31477]=13188, [31478]=24818, [31479]=19928, [31480]=23468, [31481]=13381, [31482]=16787, + [31483]=35314, [31484]=20291, [31485]=27149, [31486]=34058, [31487]=28583, [31488]=33411, + [31490]=52380, [31491]=52578, [31492]=81771, [31493]=41555, [31494]=41555, [31501]=100000, + [31508]=26847, [31509]=20212, [31510]=13186, [31511]=33940, [31512]=16606, [31513]=16669, + [31514]=41189, [31515]=20669, [31516]=20225, [31519]=50226, [31520]=37732, [31521]=36916, + [31523]=41555, [31526]=41555, [31527]=41555, [31528]=41555, [31531]=13574, [31532]=25548, + [31533]=30910, [31534]=24159, [31537]=20835, [31538]=26135, [31539]=20983, [31540]=24708, + [31541]=72250, [31542]=90633, [31543]=82287, [31544]=39983, [31545]=40134, [31546]=24173, + [31547]=36399, [31548]=56839, [31549]=57050, [31552]=26659, [31553]=26754, [31554]=35802, + [31555]=17966, [31556]=36063, [31557]=25855, [31558]=18160, [31559]=18225, [31560]=34290, + [31561]=34412, [31562]=46046, [31563]=20903, [31564]=41964, [31565]=30097, [31566]=21145, + [31567]=21809, [31568]=39399, [31569]=39721, [31570]=52922, [31571]=26559, [31572]=53308, + [31573]=38395, [31574]=26849, [31575]=26944, [31576]=47233, [31577]=47404, [31578]=63553, + [31579]=32070, [31580]=64003, [31581]=45802, [31582]=32413, [31583]=30246, [31615]=8222, + [31617]=8222, [31657]=22454, [31658]=21130, [31659]=16969, [31660]=15873, [31661]=15092, + [31666]=5000, [31670]=200, [31671]=200, [31672]=150, [31673]=150, [31674]=7500, + [31675]=7500, [31676]=5000, [31677]=5000, [31679]=3000, [31680]=10000, [31681]=10000, + [31682]=10000, [31683]=13887, [31684]=17423, [31685]=41966, [31686]=49140, [31687]=21136, + [31688]=26514, [31689]=42577, [31690]=25068, [31691]=39818, [31692]=39818, [31693]=39818, + [31694]=39818, [31695]=39818, [31696]=39818, [31699]=34629, [31700]=85828, [31701]=86148, + [31703]=69424, [31711]=20900, [31712]=24355, [31713]=29338, [31714]=23037, [31715]=28546, + [31717]=27137, [31718]=34046, [31719]=41009, [31720]=48023, [31723]=62449, [31724]=48321, + [31725]=12933, [31726]=30574, [31727]=30574, [31728]=32023, [31729]=32023, [31730]=32023, + [31731]=32023, [31732]=32023, [31733]=61828, [31734]=39721, [31735]=25, [31737]=25, + [31745]=91143, [31746]=60256, [31747]=41953, [31748]=41953, [31749]=41953, [31756]=71638, + [31758]=72181, [31759]=72453, [31760]=5000, [31761]=47720, [31762]=47893, [31764]=39664, + [31765]=46440, [31766]=19974, [31768]=18476, [31770]=11890, [31773]=2500, [31774]=2500, + [31776]=2500, [31777]=2500, [31778]=2500, [31779]=2500, [31780]=2500, [31781]=2500, + [31782]=39375, [31783]=26345, [31784]=24787, [31785]=82921, [31786]=27639, [31787]=21600, + [31788]=15396, [31789]=16773, [31790]=34629, [31791]=41555, [31792]=20526, [31793]=17169, + [31794]=44616, [31796]=19263, [31797]=24165, [31798]=25871, [31804]=2500, [31816]=82425, + [31817]=29915, [31818]=34629, [31819]=34935, [31820]=32891, [31821]=67150, [31823]=32023, + [31837]=14750, [31856]=100000, [31857]=100000, [31858]=100000, [31859]=100000, [31860]=2500, + [31861]=30000, [31862]=2500, [31863]=30000, [31864]=2500, [31865]=30000, [31866]=2500, + [31867]=30000, [31868]=30000, [31869]=2500, [31870]=15000, [31871]=15000, [31872]=15000, + [31873]=15000, [31874]=15000, [31875]=30000, [31876]=30000, [31877]=30000, [31878]=30000, + [31879]=30000, [31882]=25000, [31883]=25000, [31884]=25000, [31885]=25000, [31886]=25000, + [31887]=25000, [31888]=25000, [31889]=25000, [31890]=200000, [31891]=200000, [31892]=25000, + [31893]=25000, [31894]=25000, [31895]=25000, [31896]=25000, [31898]=25000, [31899]=25000, + [31900]=25000, [31901]=25000, [31902]=25000, [31903]=25000, [31904]=25000, [31905]=25000, + [31906]=25000, [31907]=200000, [31908]=25000, [31909]=25000, [31910]=25000, [31911]=25000, + [31912]=25000, [31913]=25000, [31914]=200000, [31915]=25000, [31916]=25000, [31917]=25000, + [31918]=25000, [31919]=51700, [31920]=51700, [31921]=51700, [31922]=51700, [31923]=51700, + [31924]=51700, [31925]=10709, [31926]=10709, [31927]=10709, [31928]=10709, [31929]=10709, + [31935]=25757, [31936]=25850, [31937]=24090, [31938]=24183, [31939]=24276, [31940]=22464, + [31942]=25775, [31943]=10709, [31944]=870, [31945]=990, [31949]=6, [31951]=2500, + [31952]=750, [31955]=12, [32062]=1250, [32063]=1500, [32067]=1500, [32068]=1500, + [32070]=10000, [32072]=38286, [32073]=57213, [32076]=33127, [32077]=33252, [32078]=50288, + [32080]=42030, [32081]=197528, [32082]=74176, [32193]=60000, [32194]=60000, [32195]=60000, + [32196]=60000, [32197]=60000, [32198]=60000, [32199]=60000, [32200]=60000, [32201]=60000, + [32202]=60000, [32203]=60000, [32204]=60000, [32205]=60000, [32206]=60000, [32207]=60000, + [32208]=60000, [32209]=60000, [32210]=60000, [32211]=60000, [32212]=60000, [32213]=60000, + [32214]=60000, [32215]=60000, [32216]=60000, [32217]=60000, [32218]=60000, [32219]=60000, + [32220]=60000, [32221]=60000, [32222]=60000, [32223]=60000, [32224]=60000, [32225]=60000, + [32226]=60000, [32227]=50000, [32228]=50000, [32229]=50000, [32230]=50000, [32231]=50000, + [32232]=50639, [32234]=43467, [32235]=58105, [32236]=145939, [32237]=146462, [32238]=60256, + [32239]=37542, [32240]=55506, [32241]=60474, [32242]=62666, [32243]=72923, [32245]=73471, + [32247]=60256, [32248]=177210, [32249]=50000, [32250]=65575, [32251]=42997, [32252]=71923, + [32253]=108266, [32254]=144879, [32255]=93057, [32256]=29185, [32257]=43930, [32258]=40965, + [32259]=41122, [32260]=88355, [32261]=60256, [32262]=138629, [32263]=87600, [32264]=63126, + [32265]=35046, [32266]=60256, [32267]=67698, [32268]=74271, [32269]=142263, [32270]=28557, + [32271]=62571, [32273]=43302, [32274]=15000, [32275]=40494, [32276]=40647, [32277]=15000, + [32278]=48060, [32279]=48240, [32280]=44050, [32281]=15000, [32282]=15000, [32283]=15000, + [32284]=15000, [32285]=15000, [32286]=15000, [32287]=15000, [32288]=15000, [32289]=15000, + [32290]=15000, [32291]=15000, [32292]=15000, [32293]=15000, [32294]=15000, [32295]=15000, + [32296]=15000, [32297]=15000, [32298]=15000, [32299]=15000, [32300]=15000, [32301]=15000, + [32302]=15000, [32303]=15000, [32304]=15000, [32305]=15000, [32306]=15000, [32307]=15000, + [32308]=15000, [32309]=15000, [32310]=15000, [32311]=15000, [32312]=15000, [32323]=42852, + [32324]=35841, [32325]=107906, [32326]=36368, [32327]=57968, [32328]=36357, [32329]=43786, + [32330]=43943, [32331]=44100, [32332]=184386, [32333]=52107, [32334]=80640, [32335]=60256, + [32336]=108233, [32337]=40787, [32338]=40943, [32339]=35187, [32340]=50057, [32341]=87600, + [32342]=50091, [32343]=107110, [32344]=179172, [32345]=75383, [32346]=43315, [32347]=36223, + [32348]=181770, [32349]=88355, [32350]=203059, [32351]=36742, [32352]=55310, [32353]=29603, + [32354]=70418, [32361]=203059, [32362]=86443, [32363]=107100, [32365]=100692, [32366]=54133, + [32367]=57951, [32368]=75000, [32369]=145925, [32370]=102777, [32373]=72100, [32374]=183948, + [32375]=94538, [32376]=62618, [32377]=52373, [32378]=9, [32381]=4000, [32387]=25096, + [32388]=146, [32389]=50288, [32390]=25241, [32391]=38007, [32392]=25432, [32393]=31912, + [32394]=48050, [32395]=32155, [32396]=64546, [32397]=38873, [32398]=58788, [32399]=39164, + [32400]=78612, [32401]=46290, [32402]=69163, [32403]=47855, [32404]=95498, [32409]=30000, + [32410]=30000, [32411]=30000, [32412]=30000, [32413]=2500, [32420]=40345, [32423]=2000, + [32428]=20000, [32429]=20000, [32430]=20000, [32431]=20000, [32432]=20000, [32433]=20000, + [32434]=20000, [32435]=20000, [32436]=20000, [32437]=20000, [32438]=20000, [32439]=20000, + [32440]=20000, [32441]=20000, [32442]=20000, [32443]=20000, [32444]=20000, [32445]=2500, + [32447]=20000, [32453]=125, [32455]=60, [32461]=69205, [32464]=1500, [32468]=1000, + [32470]=1250, [32471]=149388, [32472]=66971, [32473]=67220, [32474]=57941, [32475]=58149, + [32476]=58363, [32478]=48992, [32479]=49165, [32480]=49343, [32483]=91160, [32485]=72039, + [32486]=72039, [32487]=72039, [32488]=72039, [32489]=72039, [32490]=72039, [32491]=72039, + [32492]=72039, [32493]=72039, [32494]=38619, [32495]=38762, [32496]=91160, [32497]=60256, + [32500]=143271, [32501]=91160, [32505]=91160, [32508]=3500, [32510]=63120, [32512]=49524, + [32513]=28240, [32514]=10709, [32515]=45804, [32516]=26806, [32517]=66456, [32518]=55331, + [32519]=29614, [32520]=22464, [32521]=75350, [32522]=53945, [32524]=43636, [32525]=43804, + [32526]=60256, [32527]=60256, [32528]=60256, [32529]=30465, [32531]=41953, [32532]=22462, + [32533]=22464, [32534]=66290, [32535]=55035, [32536]=91143, [32537]=91468, [32538]=27538, + [32539]=27633, [32540]=30808, [32541]=30928, [32568]=50091, [32570]=65575, [32571]=50639, + [32573]=65575, [32574]=43624, [32575]=65964, [32577]=44091, [32579]=66904, [32580]=34533, + [32581]=51991, [32582]=34791, [32583]=52383, [32584]=28039, [32585]=42216, [32586]=28249, + [32587]=42530, [32589]=60256, [32590]=42997, [32591]=88355, [32592]=86613, [32593]=54329, + [32596]=2300, [32597]=2300, [32598]=2300, [32599]=2300, [32600]=2300, [32601]=2300, + [32606]=49713, [32608]=44050, [32609]=42835, [32620]=1250, [32624]=32200, [32625]=24150, + [32626]=32200, [32627]=24150, [32628]=32200, [32629]=32200, [32630]=24150, [32631]=24150, + [32634]=9200, [32635]=9200, [32636]=9200, [32637]=9200, [32638]=9200, [32639]=9200, + [32640]=36800, [32641]=36800, [32645]=55200, [32647]=55200, [32648]=55200, [32650]=19550, + [32651]=55200, [32652]=19550, [32653]=19550, [32654]=19550, [32655]=20699, [32656]=31165, + [32658]=10000, [32659]=85574, [32660]=88222, [32661]=88547, [32662]=111091, [32663]=111498, + [32664]=43988, [32665]=26952, [32667]=85, [32668]=320, [32681]=155, [32682]=155, + [32683]=155, [32684]=155, [32685]=400, [32686]=400, [32688]=1, [32689]=1, + [32690]=1, [32691]=1, [32692]=1, [32693]=1, [32700]=1, [32701]=1, + [32702]=1, [32703]=1, [32704]=1, [32705]=1, [32706]=1, [32707]=1, + [32708]=1, [32709]=1, [32710]=1, [32711]=1, [32712]=1, [32713]=1, + [32714]=27, [32715]=12500, [32716]=12500, [32717]=12500, [32721]=225, [32722]=200, + [32725]=2, [32727]=2, [32728]=250, [32736]=7500, [32737]=7500, [32738]=7500, + [32739]=7500, [32742]=7500, [32744]=12500, [32745]=12500, [32746]=12500, [32747]=12500, + [32748]=12500, [32749]=12500, [32750]=12500, [32751]=12500, [32752]=12500, [32753]=12500, + [32754]=12500, [32755]=12500, [32756]=80011, [32758]=2500, [32759]=8050, [32769]=20977, + [32770]=10307, [32771]=10307, [32772]=39122, [32774]=39122, [32776]=26757, [32778]=43969, + [32779]=83797, [32780]=62933, [32781]=90808, [32782]=4618, [32783]=690, [32784]=460, + [32828]=2300, [32829]=103467, [32830]=103855, [32831]=62545, [32832]=9954, [32833]=10000, + [32836]=10000, [32837]=243112, [32838]=243974, [32839]=750, [32849]=750, [32850]=750, + [32851]=750, [32852]=750, [32854]=139429, [32863]=17500, [32865]=17688, [32866]=37217, + [32867]=14257, [32868]=32343, [32869]=36876, [32870]=44412, [32871]=38924, [32872]=55914, + [32882]=6, [32883]=6, [32909]=690, [32910]=460, [32941]=97528, [32942]=97528, + [32943]=140184, [32944]=134251, [32945]=141230, [32946]=141739, [33012]=25000, [33014]=12500, + [33023]=50, [33024]=100, [33025]=125, [33026]=200, [33028]=3, [33029]=12, + [33030]=1, [33031]=25, [33032]=50, [33033]=100, [33034]=100, [33035]=160, + [33036]=160, [33042]=320, [33048]=200, [33052]=200, [33053]=200, [33054]=60256, + [33055]=60256, [33058]=60256, [33081]=5, [33092]=5000, [33093]=5000, [33117]=20000, + [33122]=38029, [33124]=12500, [33131]=30000, [33133]=30000, [33134]=30000, [33135]=30000, + [33140]=30000, [33143]=30000, [33144]=30000, [33148]=25000, [33149]=25000, [33150]=25000, + [33151]=25000, [33152]=25000, [33153]=25000, [33155]=30000, [33156]=30000, [33157]=30000, + [33158]=30000, [33159]=30000, [33160]=30000, [33165]=7500, [33173]=46380, [33174]=20000, + [33185]=18000, [33186]=950, [33191]=69481, [33203]=48289, [33204]=52547, [33205]=60000, + [33206]=57303, [33208]=5000, [33209]=10000, [33211]=33146, [33214]=137848, [33215]=94140, + [33216]=94475, [33228]=3227, [33229]=3239, [33230]=4170, [33231]=3390, [33232]=2701, + [33233]=2276, [33234]=200, [33235]=1031, [33236]=280, [33237]=5772, [33239]=5407, + [33240]=5025, [33241]=5447, [33242]=5468, [33243]=3811, [33244]=3825, [33245]=1994, + [33246]=280, [33247]=3014, [33248]=3327, [33249]=3339, [33250]=2234, [33251]=6277, + [33252]=5401, [33253]=3696, [33254]=400, [33255]=3748, [33256]=3763, [33257]=3500, + [33258]=7540, [33259]=4171, [33260]=3617, [33261]=3631, [33262]=5105, [33263]=5514, + [33264]=4728, [33265]=4378, [33266]=4378, [33267]=9547, [33268]=9582, [33269]=8905, + [33270]=9651, [33271]=10487, [33272]=9026, [33273]=7339, [33274]=6316, [33281]=102777, + [33283]=132215, [33285]=25932, [33286]=58563, [33292]=2, [33293]=60256, [33297]=88355, + [33298]=129945, [33299]=66444, [33300]=47725, [33303]=69237, [33305]=30000, [33307]=15000, + [33317]=50333, [33322]=65813, [33326]=85458, [33327]=70220, [33328]=80691, [33329]=67482, + [33332]=79001, [33354]=131461, [33356]=48278, [33357]=37012, [33388]=128494, [33389]=128987, + [33421]=73002, [33432]=60691, [33446]=43619, [33453]=40605, [33463]=42080, [33464]=59168, + [33465]=163113, [33466]=88355, [33467]=136956, [33468]=137469, [33469]=80013, [33471]=38338, + [33473]=94727, [33474]=101161, [33476]=141544, [33478]=178195, [33479]=51857, [33480]=24892, + [33481]=65461, [33489]=38619, [33490]=166140, [33491]=100053, [33492]=167354, [33493]=134375, + [33494]=168585, [33495]=135360, [33496]=60256, [33497]=60256, [33498]=60256, [33499]=60256, + [33500]=60256, [33533]=80412, [33590]=39763, [33591]=39907, [33592]=37194, [33622]=30000, + [33640]=131408, [33782]=30000, [33791]=316, [33792]=25, [33803]=2, [33804]=15000, + [33805]=55765, [33820]=25490, [33823]=8, [33824]=8, [33825]=150, [33828]=91160, + [33829]=91160, [33830]=91160, [33831]=91160, [33844]=87, [33846]=463, [33854]=563, + [33855]=3750, [33856]=33, [33857]=113, [33865]=400, [33866]=550, [33867]=8, + [33869]=5000, [33870]=5000, [33871]=5000, [33872]=150, [33873]=5000, [33874]=150, + [33875]=5000, [33924]=125, [33925]=5000, [33926]=250, [33928]=250, [33934]=11500, + [33935]=11500, [33954]=120000, [33971]=61687, [34009]=134867, [34010]=41605, [34011]=86315, + [34012]=40460, [34017]=1, [34018]=3, [34019]=12, [34020]=25, [34021]=50, + [34022]=100, [34029]=8991, [34060]=350, [34061]=375, [34063]=12, [34064]=25, + [34065]=3, [34067]=50000, [34085]=1, [34086]=1, [34087]=1, [34099]=22500, + [34100]=22500, [34105]=37500, [34106]=37500, [34109]=625, [34113]=10000, [34114]=30000, + [34130]=95, [34164]=151025, [34165]=151593, [34166]=60256, [34167]=110128, [34168]=94746, + [34169]=62571, [34170]=63625, [34172]=40000, [34173]=40000, [34174]=40000, [34175]=40000, + [34176]=132215, [34177]=88355, [34178]=86443, [34179]=203059, [34180]=107273, [34181]=61526, + [34182]=187343, [34183]=188053, [34184]=102777, [34185]=97000, [34186]=94035, [34188]=81034, + [34189]=60256, [34190]=47535, [34192]=65575, [34193]=65575, [34194]=74834, [34195]=56371, + [34196]=109851, [34197]=147035, [34198]=184504, [34199]=148171, [34200]=40000, [34201]=40000, + [34202]=46315, [34203]=154493, [34204]=88355, [34205]=46684, [34206]=203059, [34207]=7500, + [34208]=73247, [34209]=60989, [34210]=48966, [34211]=81895, [34212]=82188, [34213]=60256, + [34214]=160693, [34215]=105203, [34216]=105613, [34218]=40000, [34220]=30000, [34221]=30000, + [34227]=6200, [34228]=97222, [34229]=97572, [34230]=60256, [34231]=101743, [34232]=65744, + [34233]=64043, [34234]=41378, [34240]=55395, [34241]=48771, [34242]=48952, [34243]=85819, + [34244]=61642, [34245]=61861, [34247]=207710, [34249]=250000, [34261]=1250, [34262]=1250, + [34319]=1250, [34329]=155299, [34330]=7500, [34331]=156503, [34332]=72630, [34333]=72901, + [34334]=182930, [34335]=163190, [34336]=163793, [34337]=205493, [34339]=49674, [34340]=49855, + [34341]=44050, [34342]=33477, [34343]=50392, [34344]=33715, [34345]=88649, [34346]=160109, + [34347]=120496, [34348]=112452, [34349]=36368, [34350]=45316, [34351]=37906, [34352]=44050, + [34353]=59014, [34354]=82761, [34355]=71337, [34356]=71600, [34357]=83681, [34358]=88355, + [34359]=88355, [34360]=88355, [34361]=60256, [34362]=60256, [34363]=60256, [34364]=60849, + [34365]=61077, [34366]=30655, [34367]=30772, [34369]=77508, [34370]=38900, [34371]=78093, + [34372]=39193, [34373]=94404, [34374]=47377, [34375]=95106, [34376]=47728, [34377]=111765, + [34378]=44050, [34379]=112584, [34380]=58294, [34410]=200, [34411]=160, [34412]=50, + [34413]=2500, [34416]=4005, [34417]=5007, [34418]=20060, [34419]=25914, [34421]=12089, + [34422]=14025, [34423]=10000, [34424]=10000, [34427]=72039, [34428]=72039, [34429]=72039, + [34430]=72039, [34440]=7000, [34469]=10000, [34470]=72039, [34471]=72039, [34472]=72039, + [34473]=72039, [34476]=100000, [34482]=30000, [34484]=2500, [34486]=2500, [34490]=40000, + [34491]=24000, [34504]=6250, [34535]=2500, [34537]=2500, [34538]=1500, [34539]=1500, + [34581]=12, [34582]=12, [34601]=61631, [34602]=29513, [34603]=36689, [34604]=118908, + [34605]=85715, [34606]=122873, [34607]=33469, [34608]=139999, [34609]=121317, [34610]=48709, + [34611]=122241, [34612]=64300, [34613]=46191, [34614]=74180, [34615]=86871, [34616]=124570, + [34622]=20000, [34625]=75000, [34664]=22500, [34665]=91803, [34666]=92128, [34667]=83638, + [34670]=86938, [34671]=87263, [34672]=87589, [34673]=109893, [34674]=66173, [34675]=75568, + [34676]=75846, [34677]=58188, [34678]=58188, [34679]=58188, [34680]=58188, [34689]=30000, + [34697]=16782, [34698]=25173, [34699]=83911, [34700]=29536, [34701]=41955, [34702]=25300, + [34703]=84659, [34704]=14846, [34705]=16782, [34706]=55035, [34707]=31466, [34708]=25173, + [34780]=250, [34783]=43988, [34788]=26567, [34789]=29536, [34790]=89207, [34791]=25173, + [34792]=25173, [34793]=16782, [34794]=104889, [34795]=43969, [34796]=41955, [34797]=104889, + [34798]=7102, [34799]=64477, [34807]=56310, [34808]=21492, [34809]=42681, [34810]=32238, + [34822]=250000, [34823]=180000, [34824]=150000, [34825]=110000, [34826]=150000, [34827]=112803, + [34828]=113219, [34829]=110000, [34831]=30000, [34832]=50, [34834]=5000, [34836]=2000, + [34837]=11300, [34838]=670, [34839]=15000, [34840]=20000, [34841]=4000, [34843]=15000, + [34845]=8750, [34846]=4762, [34847]=48800, [34848]=50000, [34850]=25, [34851]=50000, + [34852]=50000, [34853]=50000, [34854]=50000, [34855]=50000, [34856]=50000, [34857]=50000, + [34858]=50000, [34859]=42800, [34860]=17000, [34861]=137, [34872]=37500, [34907]=15000, + [35128]=11250, [35181]=46676, [35182]=58564, [35183]=58777, [35184]=72681, [35185]=84940, + [35186]=20000, [35187]=37500, [35189]=37500, [35190]=37500, [35191]=37500, [35192]=37500, + [35193]=37500, [35194]=37500, [35195]=37500, [35196]=37500, [35197]=37500, [35198]=35000, + [35199]=35000, [35200]=35000, [35201]=35000, [35202]=35000, [35203]=35000, [35204]=20000, + [35205]=20000, [35206]=20000, [35207]=20000, [35208]=20000, [35209]=20000, [35210]=20000, + [35211]=20000, [35212]=20000, [35213]=20000, [35214]=20000, [35215]=20000, [35216]=20000, + [35217]=20000, [35218]=20000, [35219]=20000, [35221]=2500, [35238]=125000, [35239]=125000, + [35240]=125000, [35241]=125000, [35242]=125000, [35243]=125000, [35244]=125000, [35245]=125000, + [35246]=125000, [35247]=125000, [35248]=125000, [35249]=125000, [35250]=125000, [35251]=125000, + [35252]=125000, [35253]=125000, [35254]=125000, [35255]=125000, [35256]=125000, [35257]=125000, + [35258]=125000, [35259]=125000, [35260]=125000, [35261]=125000, [35262]=125000, [35263]=125000, + [35264]=125000, [35265]=125000, [35266]=125000, [35267]=125000, [35268]=125000, [35269]=125000, + [35270]=125000, [35271]=125000, [35273]=20000, [35275]=4618, [35282]=60256, [35283]=60256, + [35284]=60256, [35285]=50, [35286]=100, [35287]=50, [35290]=60256, [35291]=60256, + [35292]=60256, [35294]=25000, [35295]=20000, [35296]=950, [35297]=30000, [35298]=10000, + [35299]=30000, [35300]=60000, [35301]=60000, [35302]=60000, [35303]=60000, [35304]=30000, + [35305]=30000, [35306]=30000, [35307]=30000, [35308]=20000, [35309]=30000, [35310]=17500, + [35311]=20000, [35314]=75, [35315]=30000, [35316]=30000, [35318]=30000, [35322]=30000, + [35323]=30000, [35325]=30000, [35328]=17391, [35329]=26181, [35330]=35039, [35331]=26377, + [35332]=35299, [35333]=26569, [35334]=35556, [35335]=17843, [35336]=26862, [35337]=35943, + [35338]=18036, [35339]=27152, [35340]=36334, [35341]=27345, [35342]=36591, [35343]=27540, + [35344]=25693, [35345]=17194, [35346]=34518, [35347]=34648, [35356]=22381, [35357]=33693, + [35358]=45087, [35359]=33937, [35360]=42171, [35361]=21167, [35362]=31872, [35363]=42655, + [35364]=32113, [35365]=55952, [35366]=21571, [35367]=32476, [35368]=43464, [35369]=32720, + [35370]=43790, [35371]=21974, [35372]=33083, [35373]=44273, [35374]=33327, [35375]=55952, + [35376]=53709, [35377]=27651, [35378]=37657, [35379]=50405, [35380]=38118, [35381]=50795, + [35382]=25492, [35383]=38385, [35384]=51376, [35385]=38850, [35386]=51761, [35387]=25978, + [35388]=47383, [35389]=52347, [35390]=39578, [35391]=52732, [35392]=26464, [35393]=40887, + [35394]=54712, [35395]=41363, [35402]=59477, [35403]=30023, [35404]=44864, [35405]=60154, + [35406]=45200, [35407]=60610, [35408]=30589, [35409]=46929, [35410]=62919, [35411]=47270, + [35412]=63369, [35413]=31980, [35414]=47777, [35415]=64052, [35416]=48114, [35464]=35165, + [35465]=26469, [35466]=26567, [35467]=35552, [35468]=22301, [35469]=44762, [35470]=33693, + [35471]=22543, [35472]=54295, [35473]=25302, [35474]=38100, [35475]=25498, [35476]=44702, + [35477]=30143, [35478]=45043, [35487]=60000, [35488]=60000, [35489]=60000, [35494]=32525, + [35495]=32647, [35496]=32772, [35497]=32238, [35498]=15000, [35500]=37500, [35501]=30000, + [35502]=37500, [35503]=30000, [35504]=2500, [35505]=37500, [35507]=50000, [35508]=50000, + [35509]=50000, [35511]=50000, [35514]=135496, [35516]=8750, [35562]=37, [35563]=12, + [35564]=4500, [35565]=12, [35566]=4500, [35581]=26127, [35582]=15000, [35691]=237, + [35693]=15000, [35694]=12000, [35695]=62500, [35696]=62500, [35697]=62500, [35698]=62500, + [35699]=62500, [35700]=15000, [35702]=15000, [35703]=15000, [35707]=30000, [35708]=30000, + [35720]=500, [35733]=60256, [35748]=25000, [35749]=25000, [35750]=25000, [35751]=25000, + [35752]=62500, [35753]=62500, [35754]=62500, [35755]=62500, [35756]=30000, [35758]=30000, + [35759]=30000, [35760]=60000, [35761]=60000, [35762]=15000, [35763]=15000, [35764]=15000, + [35765]=15000, [35766]=125000, [35767]=125000, [35768]=125000, [35769]=125000, [35945]=350, + [36017]=3894, [36018]=7054, [36019]=13854, [37148]=8750, [37503]=60000, [37504]=125000, + [37588]=275, [37606]=1, [37710]=5000, [37915]=1250, [37934]=50000, [38082]=100000, + [38089]=40000, [38090]=50000, [38225]=23750, [38276]=12556, [38277]=1824, [38278]=1664, + [38327]=1250, [38328]=1125, [38427]=280, [38428]=400, [38429]=200, [38430]=280, + [38431]=320, [38432]=375, [38466]=500, [38506]=25500, [38518]=37, [38628]=100000, + [40000]=130, [40001]=250, [40002]=500, [40003]=38530, [40061]=20000, [40065]=1755, + [40070]=7, [40071]=7, [40072]=8, [40073]=8, [40074]=8, [40075]=5, + [40076]=5, [40077]=4, [40078]=6, [40079]=6, [40080]=48330, [40082]=375, + [40083]=850, [40084]=500, [41011]=5500, [41013]=7, [41014]=7, [41016]=12, + [41017]=10, [41018]=7, [41019]=6, [41020]=5, [41021]=10, [41023]=16, + [41024]=25, [41025]=20, [41026]=30, [41029]=32, [41034]=48, [41035]=29, + [41040]=244, [41041]=133, [41042]=425, [41043]=204, [41044]=466, [41045]=284, + [41046]=202, [41047]=190, [41048]=298, [41049]=201, [41050]=527, [41051]=388, + [41054]=285, [41055]=215, [41056]=188, [41060]=10693, [41061]=31400, [41062]=14907, + [41063]=21117, [41070]=72, [41074]=32, [41075]=72, [41076]=198954, [41077]=101212, + [41078]=2500, [41079]=2500, [41080]=2500, [41081]=2500, [41082]=2500, [41083]=2500, + [41084]=2500, [41085]=2500, [41086]=5000, [41087]=2500, [41088]=2500, [41089]=2500, + [41090]=2500, [41091]=0, [41092]=0, [41093]=2500, [41094]=2500, [41095]=2500, + [41096]=25000, [41097]=2500, [41098]=2500, [41099]=25000, [41100]=25000, [41101]=25000, + [41102]=25000, [41103]=4, [41104]=6, [41105]=4, [41106]=4, [41107]=11, + [41108]=13, [41109]=24, [41110]=30, [41111]=26, [41112]=9, [41113]=14, + [41114]=13, [41115]=7, [41116]=6, [41117]=27, [41125]=13480, [41126]=10406, + [41127]=8024, [41128]=8478, [41134]=685, [41135]=814, [41136]=425, [41148]=566, + [41149]=418, [41150]=604, [41151]=40, [41152]=55, [41153]=24, [41154]=26, + [41155]=82, [41156]=27, [41157]=43, [41158]=3, [41159]=22, [41182]=70, + [41183]=47, [41184]=241, [41185]=102, [41186]=161, [41187]=193, [41188]=195, + [41189]=146, [41190]=241, [41191]=184, [41192]=290, [41193]=139, [41194]=228, + [41195]=200, [41204]=200000, [41205]=200000, [41206]=200000, [41207]=2500, [41210]=50000, + [41211]=50000, [41212]=50000, [41213]=50000, [41214]=50000, [41215]=50000, [41216]=50000, + [41217]=50000, [41218]=50000, [41219]=50000, [41220]=50000, [41221]=50000, [41222]=50000, + [41223]=50000, [41224]=50000, [41225]=50000, [41226]=50000, [41227]=50000, [41228]=50000, + [41229]=50000, [41230]=50000, [41231]=50000, [41232]=50000, [41233]=50000, [41234]=50000, + [41235]=50000, [41236]=50000, [41237]=50000, [41238]=50000, [41239]=50000, [41240]=50000, + [41241]=50000, [41242]=50000, [41243]=50000, [41244]=50000, [41245]=50000, [41246]=50000, + [41247]=50000, [41248]=50000, [41249]=50000, [41250]=50000, [41251]=50000, [41252]=50000, + [41253]=50000, [41254]=50000, [41255]=50000, [41256]=50000, [41257]=50000, [41258]=5000, + [41259]=5000, [41260]=50000, [41261]=50000, [41262]=50000, [41263]=50000, [41264]=50000, + [41265]=50000, [41266]=50000, [41267]=50000, [41268]=50000, [41269]=50000, [41270]=50000, + [41271]=50000, [41272]=50000, [41273]=50000, [41274]=50000, [41275]=50000, [41276]=50000, + [41277]=50000, [41278]=50000, [41279]=50000, [41280]=50000, [41281]=50000, [41282]=50000, + [41283]=50000, [41284]=50000, [41285]=50000, [41286]=50000, [41287]=50000, [41288]=50000, + [41289]=50000, [41290]=50000, [41291]=50000, [41292]=50000, [41293]=50000, [41295]=32377, + [41296]=11375, [41297]=14500, [41298]=7130, [41308]=442, [41309]=418, [41310]=188, + [41311]=505, [41312]=1206, [41313]=689, [41314]=788, [41315]=2068, [41316]=751, + [41317]=3988, [41318]=612, [41319]=100, [41320]=40, [41321]=600, [41322]=400, + [41323]=2671, [41324]=4628, [41325]=500, [41326]=500, [41327]=1500, [41328]=750, + [41329]=837, [41330]=5617, [41331]=600, [41332]=200, [41340]=4403, [41341]=1250, + [41342]=2231, [41343]=4964, [41344]=80, [41345]=15836, [41346]=2177, [41347]=2644, + [41348]=2644, [41349]=3845, [41350]=1000, [41351]=1000, [41352]=1000, [41399]=20000, + [41400]=12500, [41401]=45, [41402]=45, [41427]=73, [41451]=228, [41464]=41376, + [41465]=41376, [41466]=400, [41467]=400, [41468]=400, [41469]=1, [41480]=500, + [41481]=625, [41482]=750, [41483]=4, [41485]=12500, [41495]=10000, [41497]=0, + [41498]=0, [41500]=1, [41514]=500, [41515]=500, [41516]=500, [41517]=500, + [41518]=500, [41519]=500, [41520]=500, [41521]=500, [41522]=500, [41523]=500, + [41524]=500, [41525]=500, [41526]=500, [41527]=500, [41528]=500, [41529]=500, + [41530]=500, [41531]=500, [41532]=500, [41533]=500, [41534]=500, [41535]=500, + [41536]=500, [41537]=500, [41538]=500, [41539]=500, [41540]=500, [41558]=7157, + [41559]=5068, [41560]=4688, [41561]=3818, [41562]=10968, [41563]=4019, [41564]=11855, + [41565]=2982, [41566]=2203, [41567]=2098, [41568]=1802, [41569]=1385, [41570]=1974, + [41571]=5033, [41572]=1108, [41573]=2039, [41574]=1351, [41575]=722, [41576]=151, + [41577]=109, [41578]=2208, [41579]=723, [41580]=3071, [41581]=5508, [41601]=150, + [41602]=125, [41611]=0, [41613]=0, [41615]=0, [41617]=0, [41620]=0, + [41621]=1, [41622]=0, [41623]=1, [41624]=0, [41625]=1, [41626]=0, + [41627]=0, [41628]=1, [41629]=1, [41631]=1, [41633]=1, [41635]=1, + [41637]=1, [41642]=1, [41643]=1, [41644]=1, [41645]=1, [41646]=1, + [41651]=1, [41652]=1, [41653]=1, [41654]=1, [41655]=1, [41656]=1, + [41662]=1, [41663]=1, [41664]=1, [41665]=1, [41666]=1, [41667]=0, + [41668]=0, [41669]=0, [41670]=0, [41671]=250, [41672]=125, [41673]=300, + [41674]=300, [41675]=100, [41677]=100, [41684]=1, [41692]=3, [41696]=5000, + [41698]=8750, [41702]=20415, [41703]=20398, [41707]=8750, [41709]=3256, [41713]=2866, + [41714]=2499, [41715]=6873, [41717]=10155, [41718]=3058, [41719]=3188, [41720]=2689, + [41721]=4218, [41722]=1588, [41723]=5983, [41724]=3143, [41725]=4851, [41726]=2184, + [41727]=3188, [41732]=250, [41733]=375, [41734]=125, [41736]=500, [41746]=20, + [41778]=375, [41779]=375, [41780]=375, [41785]=4932, [41786]=3878, [41787]=4371, + [41789]=2049, [41790]=8335, [41793]=10863, [41794]=3006, [41798]=6746, [41801]=2851, + [41815]=2912, [41816]=8894, [41817]=5461, [41818]=12208, [41824]=89, [41826]=782, + [41827]=572, [41828]=822, [41830]=1187, [41831]=10933, [41832]=2087, [41833]=2758, + [41836]=3357, [41838]=1808, [41839]=3247, [41840]=1197, [41842]=2903, [41843]=1822, + [41844]=1218, [41845]=12406, [41846]=4482, [41847]=2916, [41848]=3082, [41849]=22, + [41850]=31, [41851]=3121, [41852]=2, [41854]=2589, [41855]=939, [41868]=1, + [41869]=1, [41878]=1, [41879]=1431, [41880]=2115, [41881]=1207, [41882]=1910, + [41886]=1, [41887]=1, [41888]=1, [41889]=1, [41890]=1, [41891]=1, + [41892]=1, [41893]=1, [41894]=1, [41898]=148, [41899]=89, [41905]=3, + [41910]=10201, [41911]=5738, [41912]=3738, [41916]=19802, [41917]=7278, [41919]=1132, + [41930]=200, [41931]=200, [41932]=200, [41933]=200, [41934]=200, [41935]=200, + [41936]=220, [41943]=1000, [41944]=300, [41946]=5738, [41947]=5738, [41952]=5738, + [41953]=5738, [41955]=5738, [41956]=5738, [41958]=5738, [41959]=5738, [41961]=375, + [41964]=3738, [41966]=3738, [41967]=3738, [41968]=3738, [41971]=3738, [41975]=3738, + [41977]=1, [41980]=3738, [41984]=2, [45001]=250, [46600]=871, [46602]=100000, + [47000]=27956, [47001]=25485, [47002]=37144, [47003]=17054, [47004]=17249, [47005]=17119, + [47006]=33848, [47007]=25778, [47008]=34946, [47009]=31856, [47010]=46430, [47011]=21317, + [47012]=21561, [47013]=21399, [47014]=42310, [47015]=32222, [47016]=53090, [47017]=52694, + [47018]=73501, [47019]=34861, [47020]=35527, [47021]=34995, [47022]=70519, [47023]=54930, + [47024]=53090, [47025]=52694, [47026]=73501, [47027]=34861, [47028]=35527, [47029]=34995, + [47030]=70519, [47031]=54930, [47032]=72691, [47033]=57682, [47034]=124328, [47035]=88690, + [47036]=57023, [47037]=72691, [47038]=57682, [47039]=124378, [47040]=88690, [47041]=57023, + [47042]=96532, [47043]=87876, [47044]=154709, [47045]=63399, [47046]=63873, [47047]=65069, + [47048]=128228, [47049]=88203, [47050]=60256, [47051]=96532, [47052]=87876, [47053]=154709, + [47054]=63399, [47055]=63873, [47056]=65069, [47057]=128228, [47058]=88203, [47059]=60256, + [47060]=78553, [47061]=74680, [47062]=159353, [47063]=103866, [47064]=53121, [47065]=115833, + [47066]=78353, [47067]=74680, [47068]=159353, [47069]=103866, [47070]=73394, [47071]=115833, + [47072]=78553, [47073]=74680, [47074]=159353, [47075]=103866, [47076]=53121, [47077]=115833, + [47078]=34199, [47079]=31989, [47080]=42817, [47081]=21490, [47082]=21653, [47083]=21735, + [47084]=42488, [47085]=32371, [47086]=52905, [47087]=53503, [47088]=71076, [47089]=35802, + [47090]=35136, [47091]=34868, [47092]=70809, [47093]=52509, [47094]=72712, [47095]=62368, + [47096]=131062, [47097]=96608, [47098]=60567, [47099]=92895, [47100]=84586, [47101]=165217, + [47102]=64848, [47103]=62645, [47104]=64608, [47105]=136392, [47106]=84913, [47107]=60256, + [47108]=98853, [47109]=88372, [47110]=171144, [47111]=142805, [47112]=87466, [47113]=115833, + [47114]=98380, [47115]=88578, [47116]=166096, [47117]=136681, [47118]=85319, [47119]=115833, + [47120]=39111, [47121]=39582, [47122]=51956, [47123]=25879, [47124]=25781, [47125]=25682, + [47126]=52346, [47127]=38550, [47128]=39189, [47129]=39582, [47130]=51956, [47131]=25879, + [47132]=25781, [47133]=25682, [47134]=52346, [47135]=38550, [47136]=68314, [47137]=68140, + [47138]=83355, [47139]=44904, [47140]=41356, [47141]=45065, [47142]=90773, [47143]=62552, + [47144]=68314, [47145]=68140, [47146]=83355, [47147]=44904, [47148]=41356, [47149]=45065, + [47150]=90773, [47151]=62552, [47152]=80445, [47153]=70861, [47154]=152080, [47155]=108479, + [47156]=70067, [47157]=80445, [47158]=70861, [47159]=152080, [47160]=108479, [47161]=70067, + [47162]=114849, [47163]=105109, [47164]=184892, [47165]=80127, [47166]=77491, [47167]=77446, + [47168]=152688, [47169]=105504, [47170]=60256, [47171]=114949, [47172]=105109, [47173]=184892, + [47174]=80127, [47175]=77491, [47176]=79846, [47177]=152688, [47178]=105504, [47179]=60256, + [47180]=145005, [47181]=136749, [47182]=252439, [47183]=207249, [47184]=132004, [47185]=116151, + [47186]=145005, [47187]=136749, [47188]=252439, [47189]=207249, [47190]=132004, [47191]=116151, + [47192]=145005, [47193]=135749, [47194]=252439, [47195]=207249, [47196]=132004, [47197]=116151, + [47198]=34832, [47199]=31863, [47200]=42319, [47201]=21486, [47202]=22550, [47203]=21324, + [47204]=46608, [47205]=33702, [47206]=54301, [47207]=54898, [47208]=72937, [47209]=37825, + [47210]=36070, [47211]=37691, [47212]=72669, [47213]=53904, [47214]=66033, [47215]=57478, + [47216]=125316, [47217]=89392, [47218]=57256, [47219]=101212, [47220]=92129, [47221]=162862, + [47222]=63886, [47223]=68189, [47224]=63646, [47225]=134468, [47226]=92448, [47227]=60256, + [47228]=84455, [47229]=77458, [47230]=133391, [47231]=113089, [47232]=72789, [47233]=115833, + [47234]=87538, [47235]=77308, [47236]=133391, [47237]=72789, [47238]=72789, [47239]=115833, + [47240]=33962, [47241]=34206, [47242]=45119, [47243]=22233, [47244]=22397, [47245]=22477, + [47246]=45448, [47247]=33472, [47248]=56125, [47249]=55728, [47250]=75629, [47251]=36884, + [47252]=37550, [47253]=37018, [47254]=74565, [47255]=56527, [47256]=66296, [47257]=57484, + [47258]=125329, [47259]=89411, [47260]=58149, [47261]=100120, [47262]=91139, [47263]=161108, + [47264]=63158, [47265]=67468, [47266]=62918, [47267]=133025, [47268]=91466, [47269]=60256, + [47270]=78553, [47271]=74680, [47272]=159353, [47273]=103866, [47274]=53121, [47275]=115833, + [47276]=26668, [47277]=26572, [47278]=35690, [47279]=17517, [47280]=17583, [47281]=17649, + [47282]=35821, [47283]=26181, [47284]=41521, [47285]=41598, [47286]=55790, [47287]=28213, + [47288]=27573, [47289]=28106, [47290]=55575, [47291]=45551, [47292]=54661, [47293]=46868, + [47294]=101407, [47295]=72615, [47296]=47396, [47297]=78676, [47298]=71623, [47299]=126586, + [47300]=53407, [47301]=53023, [47302]=53215, [47303]=104517, [47304]=71878, [47305]=60256, + [47306]=102824, [47307]=95310, [47308]=164411, [47309]=135014, [47310]=91153, [47311]=115833, + [47312]=102824, [47313]=95310, [47314]=164411, [47315]=135014, [47316]=91153, [47317]=115833, + [47318]=144936, [47319]=136758, [47320]=252458, [47321]=207238, [47322]=131959, [47323]=115833, + [47324]=131955, [47325]=113555, [47326]=216433, [47327]=183649, [47328]=110318, [47329]=115833, + [47330]=43686, [47331]=39824, [47332]=58048, [47333]=28716, [47334]=28818, [47335]=27777, + [47336]=52893, [47337]=42924, [47338]=43686, [47339]=39824, [47340]=58048, [47341]=28716, + [47342]=28818, [47343]=27777, [47344]=52893, [47345]=42924, [47346]=67638, [47347]=68134, + [47348]=89189, [47349]=45757, [47350]=44925, [47351]=45590, [47352]=90519, [47353]=67136, + [47354]=67638, [47355]=68134, [47356]=89189, [47357]=45757, [47358]=44925, [47359]=45590, + [47360]=90519, [47361]=67136, [47362]=84126, [47363]=72948, [47364]=160220, [47365]=113438, + [47366]=73225, [47367]=84126, [47368]=72948, [47369]=160215, [47370]=113438, [47371]=73225, + [47372]=124735, [47373]=113548, [47374]=200714, [47375]=84652, [47376]=84051, [47377]=84351, + [47378]=165729, [47379]=113957, [47380]=60256, [47381]=124735, [47382]=113548, [47383]=200714, + [47384]=84652, [47385]=84051, [47386]=84351, [47387]=165729, [47388]=113957, [47389]=60256, + [47390]=130344, [47391]=124144, [47392]=210871, [47393]=171410, [47394]=116980, [47395]=115833, + [47396]=129738, [47397]=121155, [47398]=208853, [47399]=171460, [47400]=116961, [47401]=115833, + [47402]=133605, [47403]=114741, [47404]=208893, [47405]=176678, [47406]=109466, [47407]=115833, + [47408]=150, [47409]=150, [47410]=600, [47411]=25, [47412]=600, [47413]=25, + [47414]=600, [47415]=25, [48298]=13360, [49000]=0, [49992]=75, [50010]=5, + [50016]=200, [50018]=0, [50020]=125, [50021]=125, [50024]=5342, [50025]=9634, + [50028]=2040, [50030]=3, [50031]=3, [50068]=1500, [50070]=5500, [50078]=5500, + [50081]=2500, [50094]=3, [50107]=0, [50108]=0, [50109]=0, [50110]=0, + [50111]=0, [50112]=0, [50113]=0, [50114]=0, [50115]=0, [50116]=0, + [50117]=0, [50118]=0, [50119]=0, [50120]=0, [50121]=0, [50122]=0, + [50123]=0, [50124]=0, [50125]=0, [50126]=0, [50127]=0, [50128]=0, + [50129]=0, [50130]=0, [50131]=0, [50132]=0, [50133]=0, [50134]=0, + [50135]=0, [50136]=0, [50137]=0, [50138]=0, [50139]=0, [50140]=0, + [50141]=0, [50142]=0, [50143]=0, [50144]=0, [50145]=0, [50146]=0, + [50147]=0, [50148]=0, [50149]=0, [50150]=0, [50151]=0, [50152]=0, + [50153]=0, [50154]=0, [50155]=0, [50156]=0, [50157]=0, [50158]=0, + [50159]=0, [50160]=0, [50161]=0, [50162]=0, [50163]=0, [50164]=0, + [50165]=0, [50166]=0, [50167]=0, [50168]=0, [50169]=0, [50170]=0, + [50171]=0, [50186]=140, [50187]=106, [50188]=12908, [50189]=38209, [50190]=18108, + [50191]=18294, [50192]=36425, [50193]=22865, [50231]=25, [50232]=0, [50234]=3750, + [50235]=3750, [50237]=750, [50238]=3750, [50256]=1, [50257]=790, [50298]=302, + [50315]=1, [50330]=0, [50331]=0, [50332]=0, [50333]=0, [50334]=0, + [50335]=0, [50336]=0, [50378]=0, [50391]=2500, [50417]=36762, [50418]=25745, + [50427]=25281, [50428]=15746, [50429]=40281, [50430]=13211, [50431]=11311, [50432]=12214, + [50440]=73, [50480]=2500, [50481]=2500, [50520]=0, [50521]=5, [50524]=6250, + [50525]=6250, [50527]=750, [50528]=500, [50529]=500, [50531]=500, [50708]=10000, + [50745]=0, [50746]=0, [50800]=2064, [51013]=250, [51014]=250, [51015]=250, + [51016]=250, [51017]=250, [51018]=250, [51019]=250, [51021]=1, [51023]=11458, + [51024]=3244, [51025]=13874, [51029]=0, [51030]=0, [51031]=0, [51032]=0, + [51033]=0, [51034]=0, [51035]=0, [51036]=0, [51037]=0, [51038]=0, + [51039]=0, [51040]=250, [51041]=250, [51042]=250, [51043]=40000, [51045]=12875, + [51046]=49103, [51047]=16852, [51048]=15965, [51050]=0, [51051]=0, [51052]=0, + [51058]=0, [51219]=935, [51224]=1192, [51230]=0, [51245]=8, [51252]=0, + [51256]=526, [51257]=0, [51258]=0, [51262]=300, [51263]=150, [51264]=435, + [51265]=50, [51268]=75, [51269]=200, [51270]=750, [51271]=1500, [51272]=3000, + [51273]=6000, [51274]=7500, [51275]=8750, [51276]=12000, [51277]=175, [51284]=385, + [51285]=25, [51286]=125, [51300]=2300, [51301]=1000, [51310]=1500, [51320]=113, + [51332]=238, [51400]=83, [51401]=83, [51403]=142, [51435]=87, [51600]=1250, + [51601]=1250, [51602]=1250, [51603]=1250, [51604]=10000, [51605]=1250, [51606]=1250, + [51607]=1250, [51608]=1250, [51609]=1250, [51610]=1250, [51611]=1250, [51612]=1250, + [51613]=1250, [51614]=1250, [51615]=1250, [51616]=1250, [51617]=1250, [51618]=1250, + [51619]=1250, [51620]=1250, [51621]=1250, [51622]=1250, [51623]=1250, [51624]=2500, + [51625]=1250, [51626]=1250, [51627]=1250, [51628]=2500, [51629]=1250, [51630]=1250, + [51631]=1250, [51632]=1250, [51633]=1250, [51634]=1250, [51635]=250, [51636]=250, + [51637]=750, [51638]=750, [51639]=1000, [51640]=1000, [51641]=1000, [51642]=1000, + [51643]=1000, [51644]=1000, [51645]=1000, [51646]=1000, [51660]=1250, [51705]=625, + [51716]=0, [51719]=3018, [51730]=13500, [51731]=16000, [51732]=16000, [51733]=16000, + [51734]=16000, [51735]=16000, [51736]=16000, [51737]=16000, [51738]=16000, [51740]=15750, + [51741]=8457, [51742]=4291, [51743]=8641, [51744]=3589, [51745]=6564, [51746]=10814, + [51747]=3034, [51748]=4196, [51749]=3783, [51752]=15471, [51753]=9871, [51754]=7461, + [51755]=13572, [51756]=5145, [51757]=9835, [51758]=867, [51759]=2244, [51760]=741, + [51761]=652, [51762]=1708, [51763]=2892, [51764]=10291, [51765]=2438, [51766]=7039, + [51767]=4966, [51768]=3983, [51769]=6361, [51770]=6864, [51771]=9311, [51772]=4370, + [51773]=4514, [51774]=6728, [51775]=9114, [51776]=9635, [51777]=6872, [51778]=10539, + [51779]=30246, [51780]=16295, [51781]=6672, [51782]=16336, [51783]=6474, [51784]=4421, + [51785]=6939, [51786]=13565, [51787]=8229, [51788]=8173, [51789]=6864, [51790]=1496, + [51791]=452, [51792]=108, [51793]=6241, [51794]=5053, [51795]=113, [51796]=14196, + [51797]=22768, [51798]=6381, [51799]=6368, [51800]=6468, [51801]=1250, [51802]=3191, + [51803]=2969, [51804]=2461, [51805]=1986, [51809]=1250, [51810]=512, [51812]=850, + [51816]=55, [51817]=38, [51818]=425, [51819]=136, [51820]=10, [51821]=75, + [51824]=212, [51825]=368, [51827]=14, [51828]=16, [51829]=22, [51832]=3013, + [51833]=3013, [51834]=3013, [51835]=0, [51841]=5, [51842]=2700, [51843]=2400, + [51844]=2600, [51848]=303, [51849]=891, [51850]=412, [51851]=923, [51856]=250, + [51861]=700, [51862]=1063, [51864]=925, [51865]=925, [51875]=4, [51876]=4, + [51877]=4, [51880]=4, [51881]=5, [51882]=5, [51885]=5, [51886]=5, + [51887]=3, [51888]=3, [51892]=52104, [51896]=1770, [51897]=1513, [53015]=250, + [53016]=5000, [53017]=25000, [54000]=5500, [54001]=5500, [54002]=5500, [54003]=5500, + [54004]=5500, [54005]=5500, [54006]=5500, [54007]=5500, [54008]=5500, [54009]=5, + [54010]=5, [55000]=1, [55001]=1, [55002]=1, [55003]=1, [55004]=73, + [55005]=1, [55006]=1, [55007]=1, [55008]=1, [55009]=1, [55010]=1, + [55011]=1, [55012]=1, [55013]=1, [55014]=1, [55015]=1, [55016]=1, + [55017]=1, [55018]=440, [55019]=1630, [55020]=1282, [55021]=16000, [55022]=6256, + [55023]=26250, [55024]=13750, [55025]=18755, [55026]=41125, [55027]=1, [55028]=81375, + [55029]=53500, [55030]=69500, [55031]=40000, [55032]=38375, [55033]=13750, [55034]=16000, + [55035]=9375, [55036]=11250, [55037]=35878, [55038]=28135, [55039]=27773, [55040]=44180, + [55041]=72231, [55042]=40000, [55043]=45500, [55044]=13125, [55045]=7500, [55046]=35, + [55047]=7500, [55048]=35, [55049]=10000, [55050]=12000, [55051]=2500, [55052]=28125, + [55053]=7500, [55054]=33250, [55055]=2500, [55056]=40500, [55057]=25000, [55058]=45000, + [55059]=25000, [55060]=146100, [55061]=2125, [55062]=1650, [55063]=1100, [55064]=3750, + [55065]=2500, [55066]=3750, [55067]=5625, [55068]=17750, [55069]=5500, [55070]=6250, + [55072]=875, [55073]=37500, [55074]=20000, [55075]=120000, [55076]=40000, [55077]=35000, + [55078]=86443, [55079]=168703, [55080]=333929, [55081]=87461, [55082]=79120, [55083]=143687, + [55084]=92993, [55085]=136392, [55086]=94367, [55087]=102106, [55088]=63613, [55089]=77413, + [55090]=155376, [55091]=115315, [55092]=109302, [55093]=108030, [55094]=115315, [55095]=92993, + [55096]=238821, [55097]=115200, [55098]=84051, [55099]=133025, [55100]=400399, [55101]=108030, + [55102]=168703, [55103]=89033, [55104]=108652, [55105]=101212, [55106]=63886, [55107]=64569, + [55108]=101139, [55109]=170523, [55110]=86380, [55111]=108030, [55112]=86443, [55113]=110120, + [55114]=93374, [55115]=346481, [55116]=428005, [55117]=101867, [55118]=110549, [55119]=123631, + [55120]=403815, [55121]=242782, [55122]=211382, [55123]=60256, [55124]=95283, [55125]=82155, + [55126]=113884, [55127]=420581, [55128]=274010, [55129]=278357, [55130]=71496, [55131]=93668, + [55132]=88986, [55133]=259732, [55134]=184104, [55135]=205863, [55141]=2500, [55142]=2000, + [55143]=2250, [55144]=1500, [55145]=3000, [55146]=3000, [55147]=3375, [55148]=3750, + [55150]=5, [55151]=10, [55152]=100, [55153]=200, [55154]=300, [55155]=50, + [55156]=5, [55157]=9, [55158]=92, [55159]=92, [55160]=114, [55161]=124, + [55162]=138, [55163]=133, [55164]=138, [55165]=904, [55166]=149, [55167]=158, + [55168]=168, [55169]=151, [55170]=173, [55171]=206, [55172]=206, [55173]=206, + [55174]=50, [55175]=1587, [55176]=354, [55177]=2775, [55178]=7450, [55179]=4000, + [55180]=7175, [55181]=2875, [55182]=8750, [55183]=2825, [55184]=5250, [55185]=6250, + [55186]=8875, [55187]=11375, [55188]=4700, [55189]=14500, [55190]=9750, [55191]=3625, + [55192]=5500, [55193]=7000, [55194]=7250, [55195]=8250, [55196]=9500, [55197]=10500, + [55198]=12500, [55199]=12775, [55200]=15250, [55201]=16000, [55202]=6500, [55203]=11250, + [55204]=13750, [55205]=15500, [55206]=22000, [55207]=27500, [55208]=21250, [55209]=31000, + [55210]=3050, [55211]=3000, [55212]=3250, [55213]=3250, [55214]=3500, [55215]=5250, + [55216]=7000, [55217]=5275, [55218]=7000, [55219]=3500, [55220]=5225, [55221]=6875, + [55222]=5375, [55223]=7750, [55224]=12500, [55225]=13750, [55226]=28000, [55227]=53750, + [55228]=13875, [55229]=28050, [55230]=55300, [55231]=12500, [55232]=15500, [55233]=30750, + [55234]=55625, [55235]=13850, [55236]=32000, [55237]=53125, [55238]=26250, [55239]=13750, + [55240]=18000, [55241]=22000, [55242]=16250, [55243]=10750, [55244]=19500, [55245]=25, + [55246]=125, [55247]=625, [55248]=1250, [55249]=600, [55250]=1000, [55251]=1000, + [55252]=10000, [55254]=3000, [55255]=3125, [55256]=3025, [55257]=3500, [55258]=3250, + [55259]=7000, [55260]=3150, [55261]=5500, [55262]=5725, [55263]=8250, [55264]=6375, + [55265]=7500, [55266]=8750, [55267]=11000, [55268]=6000, [55269]=6250, [55270]=10225, + [55271]=10000, [55272]=12250, [55273]=2500, [55274]=59817, [55275]=90541, [55276]=207784, + [55277]=239811, [55278]=57310, [55279]=91352, [55280]=119136, [55281]=74794, [55282]=74523, + [55283]=50652, [55284]=52408, [55285]=52452, [55286]=113642, [55316]=250, [55317]=275, + [55318]=300, [55319]=350, [55320]=400, [55321]=500, [55322]=550, [55323]=1000, + [55324]=750, [55325]=925, [55326]=1250, [55327]=1750, [55328]=1925, [55329]=200, + [55330]=256, [55331]=625, [55332]=650, [55333]=300, [55334]=450, [55335]=500, + [55336]=1050, [55337]=525, [55338]=1250, [55339]=1325, [55340]=275, [55341]=800, + [55346]=288870, [55347]=373805, [55348]=368866, [55349]=263570, [55350]=83527, [55351]=63605, + [55352]=108880, [55353]=63895, [55354]=75586, [55355]=69158, [55356]=71281, [55357]=91319, + [55359]=20000, [55360]=15000, [55361]=30000, [55362]=15625, [55363]=23625, [55364]=15000, + [55365]=30550, [55366]=22500, [55367]=15625, [55368]=35000, [55369]=808, [55370]=2500, + [55371]=391, [55372]=1228, [55373]=2500, [55374]=470, [55375]=500, [55376]=2384, + [55377]=675, [55378]=411, [55379]=2140, [55380]=929, [55381]=3179, [55382]=1708, + [55383]=2421, [55384]=3740, [55385]=2208, [55386]=2506, [55387]=3125, [55388]=6513, + [55389]=3371, [55470]=2078, [55471]=11683, [55472]=6072, [55473]=6904, [55474]=6650, + [55475]=3321, [55476]=3683, [55477]=8911, [55478]=7950, [55479]=6100, [55480]=12100, + [55481]=7525, [55494]=95653, [55495]=104132, [55496]=63044, [55497]=66865, [55498]=100628, + [55499]=47790, [55500]=66200, [55501]=45674, [55502]=71306, [55503]=44352, [55504]=106552, + [55505]=0, [55506]=72533, [55507]=115403, [55508]=102728, [55509]=850, [55510]=286672, + [55511]=286672, [55512]=119685, [55513]=216386, [55514]=1324, [55515]=112200, [55516]=65875, + [55517]=67775, [55518]=35000, [55519]=34231, [55520]=45131, [55521]=42475, [55522]=42126, + [55523]=37105, [55524]=47780, [55525]=43131, [55526]=44300, [55527]=36933, [55528]=49678, + [55529]=46555, [55530]=48150, [55531]=46833, [55532]=50627, [55533]=54601, [55534]=37105, + [55535]=2500, [55536]=2500, [55537]=2500, [55538]=2500, [55539]=2500, [55540]=2500, + [55541]=2500, [55542]=2500, [55543]=2500, [55544]=2500, [55545]=2500, [55546]=2500, + [55547]=2500, [55548]=2500, [55549]=2500, [55550]=2500, [55551]=2500, [55552]=100000, + [55553]=55906, [55554]=67797, [55555]=240627, [55556]=1, [55557]=1, [55558]=1, + [55559]=1, [55560]=1, [55561]=1, [55562]=1, [55563]=1, [55564]=1, + [55565]=1, [55566]=1, [55567]=1, [55568]=1, [55569]=1, [55570]=1, + [55571]=1, [55572]=1, [55573]=1, [55574]=1, [55575]=1, [55576]=1, + [55577]=1, [55580]=80000, [56000]=600, [56001]=700, [56002]=800, [56003]=1000, + [56004]=1000, [56005]=1000, [56006]=1000, [56007]=7000, [56008]=7000, [56009]=10000, + [56010]=10000, [56011]=2000, [56012]=2000, [56013]=2000, [56014]=10000, [56015]=10000, + [56016]=10000, [56017]=10000, [56018]=10000, [56019]=160, [56020]=120, [56021]=408, + [56022]=142, [56023]=2018, [56024]=2300, [56025]=2500, [56026]=5000, [56027]=1800, + [56028]=1500, [56029]=1100, [56030]=2400, [56031]=32267, [56032]=24618, [56033]=10000, + [56034]=2687, [56035]=6712, [56036]=6048, [56037]=245, [56038]=327, [56039]=336, + [56040]=509, [56041]=1501, [56042]=2149, [56043]=2251, [56044]=205, [56045]=642, + [56046]=1382, [56047]=2048, [56048]=1945, [56049]=2749, [56050]=2481, [56051]=3077, + [56052]=11538, [56053]=7169, [56054]=2499, [56055]=4308, [56056]=1000, [56057]=7000, + [56058]=1000, [56059]=31508, [56060]=27634, [56061]=16833, [56062]=25635, [56063]=26899, + [56065]=17740, [56066]=11875, [56067]=21508, [56068]=6308, [56069]=7058, [56070]=3285, + [56071]=28837, [56072]=19414, [56073]=8300, [56074]=500, [56075]=4000, [56076]=8000, + [56077]=8000, [56089]=3108, [56090]=5011, [56091]=809, [56092]=7067, [56093]=10233, + [56094]=10633, [56095]=8305, [56096]=8688, [56097]=1000, [56098]=1000, [56099]=1000, + [56112]=10000, [58000]=5800, [58001]=13071, [58002]=6403, [58003]=4156, [58004]=4620, + [58005]=3875, [58006]=3405, [58007]=13800, [58008]=3050, [58009]=8125, [58010]=7375, + [58011]=4050, [58012]=2500, [58013]=2300, [58014]=13474, [58015]=13095, [58016]=7777, + [58017]=10297, [58018]=10405, [58019]=13589, [58020]=4701, [58021]=2782, [58022]=4951, + [58023]=2608, [58024]=5031, [58025]=6375, [58026]=5864, [58027]=5000, [58028]=3418, + [58029]=3251, [58030]=5982, [58031]=2682, [58032]=3181, [58033]=3282, [58034]=4106, + [58035]=8063, [58036]=8289, [58037]=10455, [58038]=3972, [58039]=5049, [58040]=1228, + [58041]=1226, [58042]=823, [58043]=2555, [58044]=10727, [58045]=5062, [58046]=4099, + [58047]=6011, [58048]=8169, [58049]=2100, [58050]=1355, [58051]=1625, [58052]=1550, + [58053]=1800, [58054]=131, [58055]=1825, [58056]=5650, [58057]=5800, [58058]=21802, + [58059]=4050, [58061]=3150, [58062]=1450, [58063]=4830, [58064]=3120, [58065]=2606, + [58066]=2596, [58067]=700, [58068]=1055, [58069]=12815, [58070]=6572, [58071]=2756, + [58072]=3806, [58073]=9575, [58074]=4068, [58075]=3550, [58076]=2140, [58077]=3053, + [58078]=3803, [58079]=924, [58080]=26563, [58081]=24073, [58082]=9280, [58083]=20652, + [58084]=98, [58085]=102, [58086]=56, [58087]=80, [58088]=79853, [58089]=6481, + [58090]=1920, [58091]=1955, [58092]=8802, [58093]=3069, [58094]=33195, [58095]=12813, + [58096]=1130, [58097]=950, [58098]=10700, [58099]=3581, [58100]=1570, [58101]=6303, + [58102]=5150, [58103]=2131, [58104]=1627, [58105]=2062, [58106]=2375, [58107]=2313, + [58108]=1452, [58109]=1973, [58110]=2875, [58111]=4063, [58112]=2215, [58113]=1470, + [58114]=4006, [58115]=2670, [58116]=23043, [58117]=35068, [58118]=9050, [58119]=9056, + [58120]=573, [58121]=677, [58122]=1476, [58123]=2076, [58124]=394, [58125]=288, + [58126]=800, [58127]=3050, [58128]=6380, [58129]=2975, [58130]=12815, [58131]=2055, + [58132]=2820, [58133]=2719, [58134]=1643, [58135]=11827, [58136]=3558, [58137]=6849, + [58138]=4069, [58139]=6550, [58140]=3236, [58141]=4091, [58142]=4341, [58143]=9055, + [58144]=7330, [58145]=6352, [58146]=4401, [58147]=2818, [58148]=5802, [58149]=5427, + [58150]=14425, [58151]=4841, [58152]=3975, [58153]=3045, [58154]=9396, [58155]=12553, + [58156]=4618, [58157]=4105, [58158]=9749, [58159]=4853, [58160]=8152, [58161]=11550, + [58162]=4468, [58163]=14302, [58164]=4335, [58165]=3143, [58166]=4041, [58167]=10068, + [58168]=14678, [58169]=29068, [58170]=20546, [58171]=7199, [58172]=12566, [58173]=3703, + [58174]=2424, [58175]=4237, [58176]=4728, [58177]=6644, [58178]=3168, [58179]=3125, + [58180]=2101, [58181]=3322, [58182]=14327, [58183]=5920, [58184]=4878, [58185]=9605, + [58186]=4682, [58187]=4065, [58188]=27260, [58189]=11950, [58190]=4524, [58191]=5000, + [58192]=8072, [58193]=9706, [58194]=17618, [58195]=10070, [58196]=5368, [58197]=12572, + [58198]=13206, [58199]=31870, [58200]=8821, [58201]=5436, [58202]=3988, [58203]=9320, + [58204]=5130, [58205]=61942, [58206]=23129, [58207]=113631, [58208]=28812, [58209]=91453, + [58210]=18103, [58211]=21612, [58212]=42636, [58213]=63628, [58214]=110365, [58215]=24309, + [58216]=22560, [58217]=27500, [58218]=30864, [58219]=20595, [58220]=32185, [58221]=22500, + [58222]=34129, [58223]=39537, [58224]=24789, [58225]=41103, [58226]=25821, [58227]=28594, + [58228]=33119, [58229]=23903, [58230]=27159, [58231]=36784, [58232]=55420, [58233]=61529, + [58234]=12500, [58235]=12500, [58236]=12500, [58237]=34934, [58238]=21612, [58239]=875, + [58240]=6368, [58241]=26288, [58242]=100628, [58243]=68178, [58244]=35000, [58245]=1, + [58246]=100000, [58248]=210, [58249]=855, [58250]=8167, [58251]=20664, [58252]=27131, + [58253]=23404, [58254]=31878, [58255]=36623, [58256]=26018, [58257]=49028, [58258]=32899, + [58259]=52884, [58260]=500, [58261]=2145, [58262]=11330, [58263]=9096, [58264]=2649, + [58265]=2160, [58266]=2680, [58267]=2275, [58268]=12565, [58269]=13902, [58270]=15636, + [58271]=6872, [58272]=5646, [58273]=7818, [58274]=4750, [58275]=100000, [58276]=15296, + [58277]=10644, [58278]=657, [58279]=10548, [58280]=6830, [58281]=125, [58282]=7615, + [58283]=1649, [58284]=6844, [58285]=500, [58286]=14113, [58287]=2284, [58288]=5175, + [58289]=5567, [58290]=6130, [58291]=6130, [58292]=14485, [58293]=5726, [58294]=3591, + [58295]=6218, [58296]=7545, [58297]=2572, [58298]=2075, [58299]=3882, [58300]=6222, + [58301]=2200, [58302]=150, [58304]=12500, [58305]=12500, [58400]=5000, [58401]=5000, + [59290]=1, [59291]=1, [59292]=1, [59993]=205, [59994]=205, [60001]=50, + [60002]=3750, [60003]=206967, [60004]=63630, [60005]=63478, [60006]=64102, [60007]=39228, + [60008]=53979, [60009]=41488, [60010]=113420, [60112]=2534, [60116]=12746, [60117]=17841, + [60118]=14987, [60124]=11250, [60126]=5184, [60129]=6584, [60131]=18310, [60132]=11454, + [60138]=48761, [60152]=48761, [60153]=26487, [60161]=8416, [60164]=20112, [60165]=30670, + [60169]=14266, [60173]=2461, [60175]=2461, [60179]=4306, [60180]=7104, [60181]=3945, + [60182]=4106, [60183]=5041, [60187]=10433, [60198]=3481, [60199]=809, [60200]=2694, + [60201]=5342, [60209]=11604, [60210]=6902, [60211]=10127, [60213]=7504, [60214]=3704, + [60215]=5206, [60216]=1406, [60220]=17261, [60221]=7861, [60223]=13687, [60224]=7167, + [60228]=14267, [60229]=9503, [60233]=16204, [60234]=26104, [60237]=13107, [60239]=6912, + [60258]=10214, [60268]=18406, [60269]=8207, [60271]=250, [60272]=3407, [60274]=16204, + [60275]=15837, [60277]=497, [60278]=159, [60279]=401, [60283]=7804, [60284]=11907, + [60285]=3071, [60286]=4216, [60287]=11264, [60288]=17835, [60289]=24807, [60290]=24108, + [60291]=16674, [60292]=17081, [60293]=71207, [60294]=27106, [60300]=6078, [60307]=12437, + [60308]=2873, [60309]=8409, [60316]=7981, [60317]=18134, [60318]=4587, [60333]=68714, + [60334]=14867, [60338]=516, [60339]=515, [60346]=17538, [60347]=57897, [60348]=24734, + [60350]=8064, [60351]=7586, [60352]=14521, [60353]=11331, [60354]=7785, [60355]=17221, + [60356]=16623, [60357]=14614, [60358]=8081, [60359]=7608, [60360]=13792, [60361]=11332, + [60362]=8102, [60363]=17224, [60364]=16521, [60365]=14642, [60366]=8056, [60367]=8056, + [60368]=7216, [60369]=197, [60370]=840, [60371]=821, [60379]=701, [60380]=504, + [60383]=76836, [60384]=64167, [60385]=21938, [60386]=12739, [60387]=36107, [60388]=21468, + [60389]=71243, [60390]=47638, [60392]=3259, [60393]=1137, [60394]=12238, [60395]=7638, + [60400]=7267, [60405]=13667, [60406]=9368, [60407]=25685, [60408]=4536, [60409]=4296, + [60410]=15289, [60411]=16682, [60412]=2817, [60413]=18923, [60414]=17472, [60415]=4714, + [60416]=13936, [60417]=8647, [60418]=71924, [60419]=4219, [60420]=36347, [60421]=4641, + [60422]=75842, [60423]=14256, [60424]=2974, [60425]=6712, [60426]=22893, [60427]=1320, + [60429]=11458, [60430]=5221, [60431]=8647, [60432]=4625, [60433]=16987, [60434]=14476, + [60435]=16541, [60436]=20954, [60437]=36108, [60438]=5934, [60439]=10917, [60440]=24837, + [60441]=37064, [60442]=37108, [60446]=201, [60447]=431, [60448]=1207, [60449]=1138, + [60451]=14108, [60452]=13807, [60453]=15681, [60459]=42108, [60460]=50198, [60461]=50198, + [60463]=42108, [60464]=50198, [60465]=43681, [60470]=13745, [60475]=14933, [60480]=2708, + [60481]=6437, [60485]=4000, [60486]=25000, [60487]=21108, [60488]=21108, [60489]=21487, + [60490]=21533, [60491]=50, [60492]=738, [60493]=416, [60495]=305, [60497]=49105, + [60498]=90614, [60499]=45806, [60500]=12405, [60501]=20258, [60502]=16061, [60503]=39107, + [60504]=15988, [60505]=18302, [60506]=45811, [60518]=12001, [60522]=431, [60523]=431, + [60524]=431, [60525]=6483, [60527]=23078, [60528]=6308, [60529]=9580, [60530]=15410, + [60531]=8832, [60536]=38604, [60537]=162, [60538]=237, [60539]=13899, [60540]=10836, + [60541]=11494, [60542]=13105, [60543]=40111, [60544]=40111, [60545]=30842, [60546]=67480, + [60547]=12768, [60548]=8803, [60549]=13955, [60550]=12894, [60551]=25000, [60552]=49264, + [60553]=15184, [60554]=15877, [60555]=21266, [60556]=29284, [60557]=24267, [60558]=12207, + [60559]=10000, [60560]=56680, [60561]=33107, [60562]=16609, [60563]=17134, [60564]=11134, + [60565]=41134, [60566]=8707, [60567]=13457, [60568]=14969, [60569]=14715, [60570]=15000, + [60571]=15305, [60572]=12607, [60573]=19407, [60574]=26288, [60575]=26270, [60576]=10967, + [60577]=4101, [60578]=15745, [60579]=38722, [60580]=35871, [60581]=16822, [60582]=12597, + [60585]=85, [60587]=500, [60590]=3274, [60591]=4688, [60593]=109, [60596]=4608, + [60601]=18263, [60608]=23804, [60609]=19255, [60610]=12988, [60611]=23128, [60612]=23128, + [60613]=35628, [60614]=11181, [60615]=10431, [60616]=45906, [60617]=10395, [60618]=10395, + [60620]=25000, [60622]=20144, [60624]=42207, [60625]=11407, [60626]=11682, [60629]=48164, + [60631]=7955, [60632]=9342, [60636]=15877, [60639]=6054, [60646]=13752, [60647]=13752, + [60648]=13752, [60649]=13752, [60650]=214, [60654]=307, [60656]=10977, [60657]=12416, + [60659]=7321, [60665]=15908, [60666]=16807, [60667]=16855, [60668]=10000, [60672]=14476, + [60673]=7258, [60675]=10694, [60678]=422, [60679]=225, [60684]=291, [60685]=245, + [60686]=2264, [60690]=4255, [60691]=2039, [60692]=867, [60694]=23242, [60695]=16425, + [60696]=47361, [60699]=10863, [60700]=12328, [60701]=51878, [60702]=1483, [60703]=2958, + [60704]=982, [60705]=1053, [60706]=2156, [60708]=2863, [60709]=984, [60710]=1502, + [60715]=12206, [60717]=9342, [60718]=11782, [60719]=44129, [60724]=32230, [60725]=41480, + [60726]=26328, [60727]=28227, [60728]=18227, [60729]=19231, [60730]=19552, [60734]=42773, + [60735]=10699, [60736]=14935, [60739]=15892, [60742]=15305, [60743]=46189, [60745]=103, + [60746]=533, [60747]=473, [60748]=502, [60749]=497, [60750]=456, [60751]=488, + [60752]=3223, [60755]=102, [60757]=708, [60759]=10933, [60762]=6137, [60764]=20717, + [60765]=16308, [60770]=250, [60771]=7641, [60772]=11106, [60773]=38405, [60774]=10765, + [60775]=50000, [60776]=50000, [60777]=50000, [60778]=50000, [60779]=50000, [60780]=1500, + [60781]=1250, [60782]=49103, [60783]=7078, [60784]=34603, [60785]=10363, [60786]=9603, + [60787]=43568, [60788]=6303, [60789]=10363, [60790]=36478, [60791]=11503, [60792]=28628, + [60793]=38828, [60794]=26312, [60795]=16813, [60796]=45652, [60797]=24600, [60798]=41278, + [60799]=18363, [60800]=55377, [60801]=4578, [60802]=14053, [60803]=13578, [60804]=18906, + [60805]=46344, [60806]=63803, [60807]=24352, [60808]=77500, [60809]=25380, [60818]=16705, + [60819]=7605, [60820]=12656, [60821]=9805, [60826]=1560, [60827]=1103, [60828]=2088, + [60835]=7955, [60837]=4826, [60843]=1262, [60844]=1922, [60845]=2208, [60847]=10988, + [60853]=4779, [60854]=4779, [60855]=4779, [60859]=4779, [60860]=4779, [60863]=703, + [60864]=2875, [60865]=1608, [60872]=1367, [60873]=521, [60876]=798, [60879]=5622, + [60880]=8624, [60881]=3119, [60882]=23308, [60883]=10635, [60884]=35629, [60885]=49205, + [60887]=2560, [60888]=7015, [60889]=2182, [60896]=16882, [60899]=2407, [60900]=3678, + [60901]=6087, [60902]=31245, [60903]=13699, [60904]=9208, [60905]=62106, [60907]=13869, + [60908]=18709, [60909]=14309, [60910]=30953, [60915]=2500, [60916]=2500, [60917]=2500, + [60920]=2006, [60921]=2858, [60925]=10000, [60927]=4197, [60928]=4682, [60929]=4691, + [60931]=1823, [60933]=1208, [60934]=1128, [60938]=807, [60939]=2805, [60946]=11608, + [60947]=26815, [60950]=7605, [60951]=16705, [60952]=12866, [60953]=8769, [60954]=65, + [60955]=130, [60960]=7309, [60964]=10000, [60965]=10000, [60966]=10000, [60967]=10000, + [60968]=10000, [60969]=10000, [60970]=12094, [60972]=22311, [60973]=17038, [60974]=11834, + [60975]=19504, [60976]=250, [60977]=250, [60978]=250, [60979]=10000, [60980]=10000, + [60981]=10000, [60982]=1, [60983]=600, [60984]=25, [60985]=250, [60988]=100, + [60996]=13264, [60997]=13264, [61000]=1500, [61001]=60983, [61002]=25245, [61003]=18553, + [61004]=35489, [61005]=21342, [61006]=84864, [61007]=26486, [61009]=13899, [61010]=24789, + [61011]=66457, [61012]=12154, [61013]=20163, [61014]=20164, [61015]=21963, [61016]=71193, + [61017]=27854, [61018]=20133, [61019]=45973, [61020]=61942, [61021]=16599, [61022]=62752, + [61023]=31586, [61024]=28943, [61025]=36144, [61027]=19254, [61028]=37891, [61029]=13363, + [61030]=75765, [61031]=78443, [61032]=20974, [61033]=26178, [61034]=15564, [61035]=22512, + [61036]=18442, [61037]=24355, [61038]=20478, [61039]=23441, [61040]=13869, [61041]=18494, + [61042]=65512, [61043]=33278, [61044]=76123, [61045]=23933, [61046]=62207, [61047]=18973, + [61048]=12513, [61049]=111603, [61050]=19907, [61051]=25983, [61052]=28305, [61053]=30372, + [61054]=38144, [61055]=36268, [61056]=19004, [61057]=16734, [61058]=24327, [61059]=16782, + [61060]=23345, [61061]=25303, [61062]=12506, [61063]=61862, [61064]=7752, [61065]=13557, + [61068]=68207, [61069]=16982, [61070]=13982, [61074]=60202, [61076]=9144, [61077]=25361, + [61079]=17800, [61080]=2500, [61081]=10000, [61082]=8966, [61086]=17248, [61087]=68955, + [61088]=12208, [61091]=56233, [61092]=25000, [61095]=30000, [61100]=850, [61107]=0, + [61108]=0, [61109]=0, [61154]=6345, [61155]=5673, [61156]=3290, [61163]=7348, + [61164]=9840, [61165]=6965, [61166]=9008, [61173]=175, [61174]=800, [61175]=800, + [61176]=800, [61177]=25000, [61178]=35000, [61179]=35000, [61180]=35000, [61181]=1250, + [61182]=1000, [61183]=5000, [61185]=91806, [61186]=16311, [61187]=28964, [61188]=17952, + [61189]=50000, [61190]=50000, [61191]=50000, [61192]=50000, [61193]=63755, [61194]=86933, + [61195]=86119, [61196]=8750, [61197]=5000, [61198]=1000, [61199]=5000, [61200]=500, + [61201]=600, [61202]=700, [61203]=27106, [61204]=26328, [61205]=104060, [61206]=47864, + [61207]=40831, [61208]=196725, [61209]=81613, [61210]=32107, [61211]=46805, [61212]=29834, + [61213]=40964, [61214]=51208, [61216]=2000, [61217]=7500, [61218]=7500, [61219]=7500, + [61220]=7500, [61221]=7500, [61222]=7500, [61223]=7500, [61224]=1250, [61225]=1250, + [61226]=12500, [61227]=12500, [61228]=12500, [61229]=2000, [61230]=2000, [61235]=900, + [61236]=500, [61237]=145336, [61238]=95637, [61239]=68754, [61242]=4, [61243]=66853, + [61244]=27787, [61245]=19053, [61246]=25603, [61247]=100000, [61248]=66280, [61249]=28812, + [61250]=18790, [61251]=63903, [61252]=23788, [61253]=18103, [61254]=17851, [61255]=73599, + [61256]=61590, [61257]=24353, [61259]=2655, [61260]=32279, [61261]=22043, [61262]=53363, + [61263]=21618, [61264]=107903, [61265]=46528, [61266]=25595, [61267]=21565, [61268]=63579, + [61269]=31540, [61270]=44262, [61271]=33762, [61272]=32303, [61273]=27879, [61274]=22879, + [61275]=72979, [61276]=57779, [61277]=147303, [61278]=64729, [61279]=42725, [61280]=21050, + [61281]=27179, [61282]=13679, [61283]=15805, [61284]=45342, [61285]=34654, [61286]=52728, + [61287]=30279, [61288]=15045, [61289]=21854, [61290]=14580, [61291]=27230, [61292]=22829, + [61293]=23129, [61294]=43779, [61295]=70454, [61297]=25918, [61298]=15543, [61299]=46454, + [61300]=5608, [61301]=6272, [61302]=4922, [61303]=9106, [61304]=4982, [61305]=8367, + [61306]=7159, [61307]=18209, [61308]=30266, [61309]=7155, [61310]=4323, [61311]=4087, + [61312]=25000, [61313]=9103, [61314]=15390, [61315]=30629, [61316]=8576, [61317]=8576, + [61318]=34031, [61319]=9429, [61320]=17191, [61321]=7141, [61322]=8654, [61323]=8329, + [61324]=9215, [61325]=18864, [61326]=15213, [61327]=12291, [61328]=33379, [61329]=13379, + [61330]=23153, [61331]=8608, [61332]=6402, [61333]=25138, [61334]=8153, [61335]=17241, + [61336]=4677, [61337]=2500, [61338]=30187, [61339]=8187, [61343]=7601, [61345]=4108, + [61346]=14109, [61348]=6154, [61349]=18766, [61353]=5988, [61354]=4218, [61355]=6782, + [61356]=54839, [61357]=20782, [61358]=40866, [61359]=21896, [61360]=41786, [61361]=30866, + [61362]=16635, [61363]=17694, [61364]=42531, [61365]=34688, [61366]=17608, [61367]=24309, + [61370]=150, [61371]=200, [61372]=200, [61373]=200, [61374]=150, [61375]=200, + [61376]=2981, [61377]=3761, [61378]=5899, [61379]=3042, [61383]=6788, [61384]=2508, + [61385]=4108, [61386]=404, [61389]=218, [61391]=21899, [61393]=7500, [61396]=176, + [61398]=108, [61399]=168, [61404]=203, [61405]=203, [61406]=17304, [61407]=900, + [61410]=5892, [61411]=56739, [61419]=3501, [61420]=6897, [61422]=9758, [61423]=850, + [61424]=25000, [61425]=25000, [61426]=25000, [61427]=25000, [61428]=25000, [61429]=25000, + [61430]=25000, [61431]=25000, [61432]=25000, [61433]=25000, [61434]=25000, [61435]=25000, + [61437]=7500, [61438]=7500, [61439]=15408, [61440]=10861, [61443]=26288, [61447]=28233, + [61448]=153401, [61449]=19641, [61450]=73877, [61451]=73973, [61452]=74690, [61453]=115279, + [61454]=147878, [61455]=42902, [61456]=25633, [61467]=15873, [61468]=7208, [61471]=8627, + [61472]=12809, [61475]=6844, [61476]=5931, [61478]=6851, [61481]=6277, [61483]=13896, + [61486]=3866, [61487]=5118, [61489]=10264, [61490]=6988, [61492]=4182, [61493]=2876, + [61497]=6782, [61498]=4218, [61499]=5988, [61500]=1501, [61501]=450, [61502]=1575, + [61503]=460, [61504]=878, [61505]=1578, [61506]=4623, [61507]=3925, [61508]=4138, + [61509]=25253, [61510]=11421, [61511]=7500, [61512]=5423, [61513]=6453, [61514]=8618, + [61515]=14577, [61516]=20324, [61517]=10000, [61518]=10221, [61519]=15796, [61520]=7500, + [61521]=13653, [61522]=89276, [61523]=150187, [61524]=50179, [61525]=37783, [61526]=98056, + [61527]=12500, [61528]=12322, [61529]=34185, [61530]=9144, [61531]=11000, [61532]=59953, + [61533]=29183, [61534]=51688, [61535]=14790, [61536]=5555, [61537]=34476, [61538]=43143, + [61539]=18029, [61540]=23884, [61541]=62709, [61542]=63538, [61543]=20292, [61544]=12556, + [61545]=7556, [61546]=24425, [61547]=18876, [61548]=18629, [61549]=1250, [61550]=16650, + [61551]=62187, [61552]=11393, [61553]=4450, [61554]=645, [61555]=2038, [61562]=625, + [61563]=14718, [61564]=49733, [61565]=8604, [61566]=72534, [61567]=88041, [61568]=14932, + [61569]=54398, [61570]=16357, [61571]=58340, [61572]=24170, [61573]=6057, [61574]=2499, + [61575]=4492, [61576]=2921, [61577]=23032, [61578]=9893, [61579]=7452, [61580]=7583, + [61581]=8278, [61582]=4451, [61583]=5555, [61584]=4166, [61585]=4001, [61586]=16666, + [61587]=15745, [61588]=33004, [61589]=12607, [61590]=16692, [61591]=28567, [61592]=20286, + [61593]=20871, [61594]=13888, [61595]=92495, [61596]=29323, [61597]=13578, [61598]=4871, + [61601]=16933, [61602]=5097, [61603]=7608, [61604]=7283, [61605]=7406, [61608]=11961, + [61609]=12933, [61610]=15558, [61611]=6386, [61612]=27500, [61613]=18870, [61614]=11514, + [61615]=29274, [61616]=7366, [61617]=17508, [61619]=24692, [61620]=12637, [61621]=31758, + [61622]=8864, [61623]=3208, [61627]=17966, [61628]=3581, [61629]=12209, [61630]=3618, + [61633]=16408, [61634]=5634, [61645]=36108, [61646]=24688, [61647]=14975, [61648]=8860, + [61649]=18710, [61650]=140033, [61651]=110361, [61660]=17495, [61661]=15837, [61662]=6861, + [61667]=29952, [61668]=5960, [61669]=21090, [61670]=21277, [61671]=6523, [61672]=4845, + [61673]=600, [61675]=600, [61676]=500, [61677]=3088, [61678]=2785, [61679]=6477, + [61680]=378, [61681]=298, [61682]=308, [61683]=5235, [61684]=313, [61685]=283, + [61686]=358, [61687]=608, [61688]=6098, [61689]=349, [61690]=10359, [61692]=396, + [61698]=14778, [61699]=14651, [61700]=14452, [61701]=18952, [61703]=12249, [61705]=8905, + [61707]=150, [61708]=23281, [61709]=17705, [61710]=38291, [61717]=64108, [61723]=4488, + [61725]=3815, [61726]=3853, [61727]=4781, [61729]=19271, [61730]=14131, [61732]=50000, + [61733]=50000, [61734]=5601, [61735]=28905, [61736]=6108, [61737]=12778, [61738]=1500, + [61739]=1500, [61740]=91205, [61751]=1, [61752]=1, [61753]=23388, [61754]=51204, + [61755]=71593, [61757]=20188, [61758]=26418, [61762]=4608, [61765]=14758, [61767]=10468, + [61773]=1, [61779]=10, [61780]=10, [61781]=100, [61782]=150, [61783]=200, + [61784]=200, [61785]=300, [61786]=75, [61787]=125, [61788]=300, [61789]=500, + [61790]=1250, [61791]=3750, [61792]=37500, [61799]=25000, [61802]=200, [61803]=12500, + [61805]=12500, [61806]=12500, [61807]=12500, [61808]=12500, [61809]=12500, [61810]=650, + [61816]=86504, [61818]=8000, [61819]=0, [61820]=0, [62000]=5500, [62001]=5500, + [62002]=10000, [62003]=30000, [62004]=30000, [62005]=30000, [62006]=30000, [62007]=30000, + [62008]=1831, [65000]=26973, [65001]=23154, [65002]=22563, [65003]=34934, [65004]=99743, + [65005]=28816, [65006]=52973, [65007]=5326, [65008]=87953, [65009]=4792, [65010]=195, + [65011]=1114, [65012]=2752, [65013]=9306, [65014]=32182, [65015]=19678, [65016]=100, + [65017]=250, [65018]=300, [65019]=17254, [65021]=50109, [65022]=13245, [65023]=8893, + [65024]=43089, [65025]=43096, [65026]=32076, [65027]=31814, [65028]=404, [65029]=11750, + [65030]=3253, [65031]=5000, [65032]=300, [65035]=32178, [65036]=60404, [65037]=60299, + [65038]=25742, [65039]=61151, [65100]=55305, [65101]=28031, [65102]=43440, [65103]=96056, + [65104]=43440, [65105]=28031, [68068]=1, [68069]=1, [68070]=0, [69000]=1500, + [69001]=1500, [69002]=1500, [69003]=1500, [69100]=0, [69101]=0, [69102]=0, + [69103]=0, [69104]=1, [69105]=1, [69106]=1, [69109]=30000, [69110]=30000, + [69111]=30000, [69112]=30000, [69113]=10000, [69114]=10000, [69115]=10000, [69116]=10000, + [69117]=0, [69118]=0, [69119]=0, [69120]=0, [69121]=0, [69122]=0, + [69123]=0, [69124]=0, [69125]=0, [69126]=55, [69127]=0, [69128]=0, + [69129]=0, [69130]=0, [69131]=0, [69132]=0, [69133]=12500, [69134]=12500, + [69135]=12500, [69136]=12500, [69137]=12500, [69138]=12500, [69139]=12500, [69140]=12500, + [69141]=12500, [69142]=12500, [69143]=12500, [69144]=12500, [69145]=12500, [69146]=0, + [69147]=0, [69148]=0, [69149]=0, [69150]=0, [69151]=0, [69152]=0, + [69153]=5000, [69154]=5000, [69155]=5000, [69156]=5000, [69157]=5000, [69158]=5000, + [69159]=5000, [69160]=5000, [69161]=5000, [69162]=5000, [69163]=5000, [69164]=5000, + [69165]=5000, [70000]=25000, [70001]=25000, [70002]=25000, [70003]=1253, [70004]=1331, + [70005]=1846, [70006]=1843, [70008]=1648, [70009]=3409, [70010]=5912, [70011]=5842, + [70012]=3187, [70014]=1781, [70016]=2500, [70032]=935, [70033]=1025, [70034]=1030, + [70036]=1425, [70037]=2045, [70045]=4515, [70046]=915, [70047]=2846, [70048]=909, + [70049]=1, [70050]=29712, [70051]=18033, [70052]=14586, [70053]=34212, [70054]=15684, + [70055]=27511, [70056]=24512, [70057]=24512, [70058]=27511, [70059]=15684, [70060]=34212, + [70062]=18033, [70063]=14586, [70064]=29712, [70070]=2551, [70101]=12500, [70102]=12500, + [70103]=625, [70104]=1400, [70105]=1800, [70106]=2400, [70107]=2200, [70108]=2350, + [70109]=2700, [70110]=450, [70111]=525, [70112]=525, [70113]=600, [70114]=650, + [70115]=600, [70116]=600, [70117]=200, [70118]=600, [70119]=700, [70120]=800, + [70125]=1400, [70126]=125, [70127]=1000, [70128]=1100, [70129]=1800, [70138]=1100, + [70141]=125, [70142]=150, [70143]=150, [70144]=175, [70145]=175, [70146]=225, + [70147]=250, [70148]=500, [70149]=330, [70150]=75, [70151]=400, [70152]=150, + [70153]=300, [70154]=375, [70155]=150, [70159]=650, [70160]=1450, [70161]=700, + [70162]=2200, [70163]=2000, [70164]=750, [70166]=8500, [70167]=1400, [70168]=500, + [70169]=1250, [70170]=7500, [70171]=7500, [70172]=7500, [70173]=7500, [70174]=7500, + [70175]=7500, [70176]=5500, [70177]=5500, [70178]=5500, [70179]=5000, [70180]=2850, + [70181]=2500, [70182]=5000, [70183]=6250, [70184]=3250, [70185]=200, [70186]=625, + [70187]=500, [70188]=215, [70189]=500, [70190]=950, [70191]=1400, [70192]=550, + [70193]=1200, [70194]=1200, [70195]=1050, [70196]=1400, [70197]=1250, [70198]=12500, + [70199]=650, [70200]=1800, [70201]=13500, [70202]=13500, [70203]=2000, [70204]=400, + [70205]=1250, [70206]=1200, [70207]=4800, [70208]=485, [70209]=12500, [70210]=11500, + [70211]=22500, [70212]=7250, [70213]=13500, [70214]=12500, [70215]=15250, [70216]=7500, + [70217]=10000, [70218]=7250, [70219]=17500, [70220]=5000, [70221]=4500, [70222]=8500, + [70223]=8500, [70224]=689, [70225]=138, [70226]=25000, [70239]=598, [70240]=515, + [70241]=50, [70597]=34199, [70598]=31989, [70599]=42817, [70600]=21490, [70601]=21653, + [70602]=21735, [70603]=42488, [70604]=32356, [70613]=52905, [70614]=53503, [70615]=71076, + [70616]=35802, [70617]=35136, [70618]=34868, [70619]=70809, [70620]=52509, [70626]=72712, + [70627]=62368, [70628]=131062, [70629]=96608, [70630]=60567, [70640]=92895, [70641]=84586, + [70642]=165217, [70643]=64848, [70644]=62645, [70645]=64608, [70646]=136392, [70647]=84913, + [70648]=60256, [70732]=66033, [70733]=57478, [70734]=125316, [70735]=89392, [70736]=57257, + [70739]=161108, [70744]=91466, [70783]=91139, [80000]=63250, [80001]=63250, [80003]=63250, + [80006]=1500, [80007]=63250, [80010]=1000, [80060]=0, [80101]=1, [80102]=1, + [80104]=10, [80105]=1, [80106]=1, [80107]=1, [80108]=3, [80109]=3, + [80110]=3, [80111]=3, [80113]=5, [80115]=3, [80116]=3, [80117]=3, + [80118]=3, [80120]=4, [80121]=4, [80122]=5, [80123]=5, [80124]=5, + [80125]=5, [80126]=59, [80127]=53, [80128]=78, [80129]=78, [80131]=51, + [80132]=45, [80133]=51, [80156]=1, [80157]=5, [80158]=6, [80159]=4, + [80160]=6, [80161]=5, [80162]=6, [80163]=5, [80164]=6, [80165]=2, + [80166]=3, [80167]=1, [80168]=1, [80202]=1, [80203]=1, [80204]=3, + [80205]=3, [80206]=1, [80207]=1, [80208]=1, [80211]=5, [80212]=3, + [80213]=3, [80214]=3, [80215]=3, [80217]=3, [80218]=3, [80219]=4, + [80220]=4, [80222]=1768, [80223]=1825, [80234]=2, [80250]=1, [80251]=1, + [80300]=25000, [80301]=20000, [80304]=20000, [80305]=10000, [80308]=15000, [80312]=15000, + [80316]=15000, [80320]=10000, [80400]=1500, [80433]=0, [80438]=0, [80446]=0, + [80457]=0, [80458]=0, [80459]=0, [80460]=20000, [80461]=20000, [80462]=20000, + [80463]=2500, [80464]=2277, [80465]=2277, [80466]=2277, [80500]=125, [80501]=100, + [80502]=150, [80503]=100, [80504]=150, [80505]=2068, [80506]=8761, [80507]=10310, + [80508]=10239, [80509]=10281, [80510]=10214, [80511]=10295, [80512]=10310, [80513]=10236, + [80514]=10217, [80515]=10212, [80516]=10296, [80517]=10299, [80518]=10242, [80519]=10304, + [80520]=10216, [80521]=10273, [80522]=10306, [80523]=10237, [80524]=10284, [80525]=10216, + [80526]=10269, [80527]=5638, [80528]=23714, [80529]=23497, [80530]=23542, [80531]=33912, + [80532]=23424, [80533]=23543, [80534]=33972, [80535]=23581, [80536]=23610, [80537]=33978, + [80538]=33941, [80539]=17916, [80540]=17922, [80541]=33913, [80542]=33897, [80543]=23506, + [80544]=18871, [80545]=18863, [80546]=21783, [80547]=33972, [80600]=125, [80601]=100, + [80602]=150, [80603]=100, [80604]=150, [80605]=2068, [80606]=8761, [80607]=10310, + [80608]=10239, [80609]=10281, [80610]=10214, [80611]=10295, [80612]=10310, [80613]=10236, + [80614]=10217, [80615]=10212, [80616]=10296, [80617]=10299, [80618]=10242, [80619]=10304, + [80620]=10216, [80621]=10273, [80622]=10306, [80623]=10237, [80624]=10284, [80625]=10216, + [80626]=10269, [80627]=5638, [80628]=23714, [80629]=23497, [80630]=23542, [80631]=33912, + [80632]=23424, [80633]=23543, [80634]=33972, [80635]=23581, [80636]=23610, [80637]=33978, + [80638]=33941, [80639]=17916, [80640]=17922, [80641]=33913, [80642]=33897, [80643]=23506, + [80644]=18871, [80645]=18863, [80646]=21783, [80649]=2502, [80650]=0, [80651]=0, + [80652]=0, [80653]=0, [80654]=0, [80655]=0, [80656]=0, [80657]=0, + [80658]=0, [80659]=0, [80660]=0, [80661]=0, [80662]=0, [80663]=0, + [80664]=0, [80665]=0, [80670]=8235, [80671]=29105, [80672]=10382, [80673]=16618, + [80674]=10667, [80700]=1207, [80701]=560, [80702]=359, [80703]=311, [80704]=622, + [80705]=1264, [80706]=611, [80707]=754, [80708]=905, [80709]=2381, [80710]=219, + [80711]=687, [80712]=927, [80713]=1134, [80714]=867, [80715]=782, [80716]=4465, + [80717]=1614, [80718]=1821, [80719]=1672, [80720]=1270, [80721]=751, [80722]=1962, + [80723]=2596, [80724]=1472, [80725]=874, [80726]=1359, [80727]=2347, [80728]=2582, + [80729]=1272, [80730]=1293, [80731]=2954, [80732]=1497, [80733]=7864, [80734]=4787, + [80735]=1243, [80736]=3765, [80737]=3391, [80738]=2581, [80739]=7753, [80740]=6984, + [80741]=2067, [80742]=2475, [80743]=1543, [80744]=14472, [80745]=12270, [80746]=12836, + [80747]=5762, [80748]=5863, [80749]=1866, [80750]=8369, [80751]=8868, [80752]=2430, + [80753]=2382, [80754]=3363, [80755]=1777, [80756]=2820, [80758]=6244, [80759]=4085, + [80760]=7864, [80761]=4536, [80763]=15289, [80765]=2817, [80766]=4714, [80767]=13936, + [80769]=5221, [80770]=4219, [80771]=4489, [80772]=10320, [80773]=4641, [80774]=2865, + [80775]=14691, [80776]=6712, [80777]=2974, [80778]=2144, [80779]=8372, [80780]=3837, + [80781]=18226, [80782]=3037, [80783]=6003, [80784]=2189, [80785]=8632, [80786]=2675, + [80787]=2687, [80789]=1874, [80790]=3512, [80791]=3342, [80792]=3402, [80793]=4470, + [80794]=1939, [80795]=7418, [80796]=401, [80797]=1336, [80798]=2239, [80799]=2724, + [80801]=6224, [80802]=956, [80803]=1179, [80804]=3276, [80805]=6986, [80806]=1163, + [80807]=789, [80809]=1855, [80810]=1352, [80811]=814, [80812]=13440, [80813]=75, + [80814]=25, [80815]=1145, [80816]=74, [80817]=179, [80818]=41, [80819]=104, + [80820]=95, [80821]=80, [80822]=451, [80823]=2693, [80824]=2277, [80825]=4189, + [80826]=2744, [80827]=552, [80828]=685, [80829]=1278, [80830]=869, [80831]=305, + [80832]=604, [80833]=454, [80834]=1528, [80835]=578, [80836]=2326, [80837]=1157, + [80838]=1157, [80839]=465, [80840]=1358, [80841]=3702, [80842]=5237, [80843]=4098, + [80844]=3043, [80845]=1896, [80846]=1974, [80847]=1981, [80848]=1866, [80849]=4743, + [80850]=4121, [80851]=10416, [80852]=4286, [80853]=7844, [80854]=2843, [80860]=891, + [80861]=628, [80866]=0, [80876]=5, [80878]=1500, [81003]=18613, [81004]=26686, + [81005]=484, [81006]=489, [81007]=163, [81008]=550, [81009]=3500, [81010]=8000, + [81011]=12500, [81012]=100, [81014]=1531, [81015]=1707, [81016]=7836, [81017]=3364, + [81018]=13143, [81019]=3785, [81020]=305, [81021]=226, [81024]=2500, [81025]=2500, + [81030]=92, [81031]=108, [81032]=20, [81050]=3014, [81051]=2762, [81052]=6764, + [81053]=2762, [81054]=3183, [81055]=4455, [81056]=5262, [81057]=15262, [81059]=1500, + [81060]=180744, [81061]=7089, [81062]=11841, [81063]=10989, [81064]=14283, [81065]=10187, + [81066]=20195, [81080]=324, [81089]=15000, [81092]=214, [81093]=92, [81094]=100, + [81095]=1, [81101]=0, [81110]=2632, [81111]=0, [81112]=0, [81113]=1632, + [81114]=1632, [81122]=3956, [81123]=5337, [81124]=5024, [81125]=3918, [81131]=5024, + [81160]=10000, [81167]=51201, [81182]=0, [81183]=13750, [81185]=0, [81186]=0, + [81187]=1750, [81190]=0, [81191]=0, [81192]=0, [81193]=0, [81194]=0, + [81195]=0, [81196]=8750, [81198]=0, [81199]=18641, [81211]=284, [81212]=579, + [81213]=811, [81214]=459, [81215]=886, [81216]=441, [81217]=558, [81218]=21353, + [81219]=16228, [81220]=9403, [81221]=12300, [81222]=9403, [81223]=50867, [81224]=0, + [81225]=0, [81226]=0, [81233]=0, [81237]=0, [81241]=0, [81243]=2500, + [81244]=0, [81245]=0, [81246]=0, [81247]=0, [81248]=1500, [81250]=0, + [81251]=0, [81252]=0, [81253]=0, [81254]=1500, [81257]=2500, [81260]=21786, + [81261]=6497, [81262]=6541, [81263]=12341, [81264]=10201, [81265]=5496, [81266]=1484, + [81267]=239, [81268]=1243, [81269]=844, [81270]=702, [81271]=16543, [81272]=19013, + [81276]=1, [81283]=1500, [81288]=13658, [81289]=15000, [81290]=5692, [81294]=112, + [81295]=108, [81296]=37, [81297]=50, [81298]=5000, [81299]=100, [81300]=100, + [81301]=100, [81302]=100, [81303]=100, [81304]=2000, [81305]=87500, [81306]=43894, + [81307]=19513, [81308]=18750, [81309]=43894, [81310]=40000, [81311]=31250, [81316]=2706, + [81317]=1183, [81319]=6332, [81320]=6738, [81321]=5622, [81325]=31255, [81328]=11294, + [81329]=16822, [81330]=12833, [81339]=42113, [81340]=100, [81341]=2606, [81342]=3516, + [81349]=1500, [81350]=19201, [81351]=24133, [81352]=7622, [81353]=7622, [81354]=8123, + [81355]=16422, [81356]=2533, [81357]=12400, [81360]=41255, [81361]=21422, [81362]=12544, + [81363]=61222, [81364]=10611, [81365]=26811, [81366]=24511, [81367]=24566, [81368]=17487, + [81369]=97271, [81370]=13812, [81371]=23954, [81372]=46635, [81373]=12652, [81374]=28173, + [81375]=67231, [81376]=13841, [81377]=15128, [81378]=13527, [81379]=62841, [81380]=13764, + [81381]=12933, [81382]=7424, [81383]=34764, [81384]=10127, [81402]=2114, [81411]=4591, + [81416]=2000, [81417]=2000, [81418]=2000, [81500]=15563, [81501]=15563, [81502]=15563, + [81503]=15563, [82950]=25821, [82951]=25821, [82952]=25821, [82953]=25821, [82954]=891, + [82955]=309, [83000]=12500, [83005]=8048, [83007]=1, [83008]=375, [83009]=1, + [83060]=64531, [83061]=64531, [83062]=64531, [83063]=64531, [83064]=6783, [83065]=11245, + [83066]=11245, [83067]=11245, [83068]=11245, [83069]=11245, [83070]=11245, [83071]=11245, + [83072]=11245, [83073]=11245, [83074]=11245, [83075]=11245, [83076]=11245, [83077]=11245, + [83078]=11245, [83079]=28764, [83080]=28764, [83081]=3462, [83082]=16438, [83093]=68652, + [83094]=40, [83200]=11043, [83201]=1753, [83202]=1747, [83203]=6703, [83204]=2312, + [83205]=7411, [83206]=2385, [83207]=3862, [83208]=3321, [83209]=14485, [83210]=2025, + [83211]=4154, [83212]=5513, [83213]=3089, [83214]=12637, [83215]=9601, [83216]=6443, + [83217]=8741, [83218]=3805, [83219]=13062, [83220]=9278, [83221]=15611, [83222]=3105, + [83223]=3841, [83224]=5000, [83225]=9103, [83226]=3285, [83227]=5864, [83228]=6105, + [83229]=10144, [83230]=5841, [83231]=14211, [83232]=3194, [83233]=31674, [83234]=32142, + [83235]=100000, [83236]=4762, [83237]=63541, [83238]=63371, [83239]=63522, [83240]=63677, + [83241]=13216, [83242]=17144, [83243]=8964, [83244]=13248, [83250]=5609, [83251]=6872, + [83253]=2906, [83254]=2612, [83255]=3287, [83256]=811, [83257]=42688, [83258]=36815, + [83259]=68731, [83260]=40157, [83261]=46612, [83262]=69285, [83263]=18244, [83264]=34624, + [83265]=26488, [83266]=16841, [83267]=18742, [83268]=14289, [83269]=89412, [83273]=9987, + [83274]=21355, [83275]=10864, [83276]=13244, [83277]=9588, [83278]=13486, [83279]=24611, + [83280]=7638, [83281]=9408, [83282]=4731, [83283]=4972, [83284]=3271, [83285]=6034, + [83286]=4107, [83287]=6213, [83288]=3681, [83289]=2271, [83290]=2734, [83291]=5012, + [83292]=9671, [83293]=10402, [83294]=16204, [83295]=6287, [83296]=8898, [83297]=12864, + [83309]=250, [83400]=5609, [83401]=6872, [83402]=6400, [83403]=2906, [83404]=2612, + [83405]=3287, [83406]=421, [83407]=629, [83408]=294, [83409]=764, [83410]=6301, + [83411]=4164, [83412]=9604, [83413]=9416, [83414]=7345, [83415]=9131, [83416]=29461, + [83417]=31340, [83420]=13087, [83421]=7543, [83423]=14559, [83424]=11318, [83425]=8085, + [83426]=17243, [83427]=16656, [83428]=14531, [83429]=13066, [83430]=7563, [83431]=14480, + [83432]=12014, [83433]=6910, [83434]=17243, [83435]=16636, [83436]=14587, [83440]=76809, + [83441]=24897, [83442]=24301, [83443]=27681, [83444]=37891, [83445]=64507, [83446]=59364, + [83447]=15763, [83448]=17438, [83449]=19106, [83450]=17309, [83451]=17781, [83452]=46108, + [83453]=36109, [83454]=28309, [83455]=13692, [83456]=17804, [83457]=13365, [83458]=11283, + [83459]=12934, [83460]=16308, [83461]=75106, [83462]=36809, [83463]=11460, [83464]=25108, + [83465]=17319, [83466]=24367, [83467]=46075, [83468]=19208, [83469]=18694, [83470]=37108, + [83471]=73208, [83472]=18608, [83478]=55, [83479]=55, [83480]=64080, [83481]=64208, + [83482]=64115, [83483]=41115, [83484]=78472, [83485]=23615, [83486]=38679, [83487]=122229, + [83488]=58777, [83490]=12500, [83491]=12500, [83492]=12500, [83493]=12500, [83494]=10875, + [83495]=10625, [83496]=16250, [83500]=25000, [83501]=25000, [83502]=25000, [83503]=25000, + [83504]=25000, [83505]=25000, [83506]=25000, [83507]=2500, [83510]=250, [83511]=250, + [83513]=59100, [83514]=18074, [83515]=13682, [83517]=352, [83518]=3000, [83519]=150, + [83520]=0, [83530]=43258, [83531]=33168, [83532]=34858, [83533]=2500, [83534]=1500, + [83535]=1625, [83536]=1750, [83537]=3000, [83538]=2500, [83540]=2500, [83541]=532, + [83542]=532, [83543]=532, [83544]=10000, [83545]=10000, [83546]=15000, [83547]=10000, + [83548]=10000, [83549]=1000, [83550]=20000, [83551]=20000, [83552]=20000, [83553]=20000, + [83554]=20000, [83555]=20000, [83556]=20000, [83557]=20000, [83558]=50000, [83559]=50000, + [83560]=50000, [83561]=50000, [83562]=123605, [83563]=103754, [83564]=103562, [83565]=44873, + [83570]=50000, [83571]=80000, [83572]=100000, [83573]=100000, [83574]=100000, [83575]=100000, + [83584]=4000, [84000]=195853, [84001]=89578, [84002]=61963, [84003]=60251, [84004]=76304, + [84005]=91152, [84006]=95175, [84010]=3000, [84011]=3000, [84012]=3500, [84013]=3500, + [84014]=3500, [84015]=3500, [84017]=2, [84030]=265375, [84031]=102106, [84032]=103086, + [84033]=155376, [84034]=48802, [84035]=62973, [84036]=115315, [84037]=3305, [84039]=1162, + [84040]=300, [84041]=300, [84500]=139779, [84501]=49658, [84502]=47308, [84503]=184893, + [84504]=45914, [84505]=40572, [84506]=60256, [84507]=55278, [84509]=19958, [84600]=84954, + [84601]=21128, [84602]=52906, [84603]=79312, [84604]=18500, [84605]=6, [91761]=81, + [91763]=1250, [91765]=3145, [91777]=86504, [91790]=37, [91796]=30000, [91797]=30000, + [92000]=250, [92001]=100000, [92002]=100000, [92003]=100000, [92004]=100000, [92016]=2500, + [92025]=5000, [92045]=7500, [92053]=7, [92055]=0, [92060]=25, [92062]=0, + [92075]=0, [92079]=0, [93061]=0, [93088]=0, [93092]=37, [93093]=37, + [93094]=2500, [93095]=2500, [93096]=37 +} diff --git a/SetupWizard.lua b/SetupWizard.lua new file mode 100644 index 0000000..ab3c7b2 --- /dev/null +++ b/SetupWizard.lua @@ -0,0 +1,1257 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Setup Wizard (SetupWizard.lua) +-- First-run guided configuration & re-run from ConfigUI +-------------------------------------------------------------------------------- + +SFrames.SetupWizard = {} +local SW = SFrames.SetupWizard + +-------------------------------------------------------------------------------- +-- Local state +-------------------------------------------------------------------------------- +local overlay, panel, contentScroll, contentChild +local headerTitle, stepLabel +local btnPrev, btnNext, btnSkip +local stepDots = {} +local stepPages = {} +local stepBuilders = {} +local stepList = {} +local currentStep = 1 +local runMode = "firstrun" +local completeCb = nil +local choices = {} +local built = false + +local PANEL_W, PANEL_H = 620, 470 +local CONTENT_W = PANEL_W - 40 +local CONTENT_H = PANEL_H - 110 + +local wid = 0 +local function WN(p) wid = wid + 1; return "NanamiWiz" .. p .. wid end + +local function Clamp(v, lo, hi) + if v < lo then return lo end + if v > hi then return hi end + return v +end + +local function T() return SFrames.ActiveTheme end + +-------------------------------------------------------------------------------- +-- Lightweight widget helpers (self-contained, mirror ConfigUI style) +-------------------------------------------------------------------------------- + +local function HideTex(tex) + if not tex then return end + if tex.SetTexture then tex:SetTexture(nil) end + tex:Hide() +end + +-- Styled button (matches ConfigUI StyleButton) +local function MakeButton(parent, text, w, h, onClick) + local btn = CreateFrame("Button", WN("Btn"), parent, "UIPanelButtonTemplate") + btn:SetWidth(w); btn:SetHeight(h) + btn:SetText(text) + btn:SetScript("OnClick", onClick) + + local t = T() + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + HideTex(btn:GetNormalTexture()) + HideTex(btn:GetPushedTexture()) + HideTex(btn:GetHighlightTexture()) + HideTex(btn:GetDisabledTexture()) + local nm = btn:GetName() or "" + for _, suf in ipairs({"Left","Right","Middle"}) do + local tx = _G[nm..suf]; if tx then tx:SetAlpha(0); tx:Hide() end + end + + local function SetVis(state) + local bg, bd, tx = t.buttonBg, t.buttonBorder, t.buttonText + if btn.sfActive then + bg, bd, tx = t.buttonActiveBg, t.buttonActiveBorder, t.buttonActiveText + elseif state == "hover" then + bg = t.buttonHoverBg; bd = t.btnHoverBd or t.buttonActiveBorder; tx = t.buttonActiveText + elseif state == "down" then bg = t.buttonDownBg + elseif state == "disabled" then bg = t.buttonDisabledBg; tx = t.buttonDisabledText end + if btn.SetBackdropColor then btn:SetBackdropColor(bg[1],bg[2],bg[3],bg[4]) end + if btn.SetBackdropBorderColor then btn:SetBackdropBorderColor(bd[1],bd[2],bd[3],bd[4]) end + local fs = btn:GetFontString(); if fs then fs:SetTextColor(tx[1],tx[2],tx[3]) end + end + btn.RefreshVisual = function() + if btn.sfActive then SetVis("active") + elseif btn.IsEnabled and not btn:IsEnabled() then SetVis("disabled") + else SetVis("normal") end + end + btn:SetScript("OnEnter", function() if this.IsEnabled and this:IsEnabled() and not this.sfActive then SetVis("hover") end end) + btn:SetScript("OnLeave", function() if this.IsEnabled and this:IsEnabled() and not this.sfActive then SetVis("normal") end end) + btn:SetScript("OnMouseDown", function() if this.IsEnabled and this:IsEnabled() and not this.sfActive then SetVis("down") end end) + btn:SetScript("OnMouseUp", function() if this.IsEnabled and this:IsEnabled() and not this.sfActive then SetVis("hover") end end) + btn:RefreshVisual() + return btn +end + +local function MakeCheck(parent, label, x, y, getter, setter, onChange) + local name = WN("Chk") + local cb = CreateFrame("CheckButton", name, parent, "UICheckButtonTemplate") + cb:SetWidth(18); cb:SetHeight(18) + cb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + + HideTex(cb:GetNormalTexture()); HideTex(cb:GetPushedTexture()) + HideTex(cb:GetHighlightTexture()); HideTex(cb:GetDisabledTexture()) + if cb.SetCheckedTexture then cb:SetCheckedTexture("") end + if cb.SetDisabledCheckedTexture then cb:SetDisabledCheckedTexture("") end + + local t = T() + local box = CreateFrame("Frame", nil, cb) + box:SetPoint("TOPLEFT", cb, "TOPLEFT", 0, 0) + box:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 0, 0) + box:SetFrameLevel(cb:GetFrameLevel()+1) + box:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + + local function ApplyVis(checked) + if checked then + box:SetBackdropColor(t.checkFill[1], t.checkFill[2], t.checkFill[3], 0.88) + box:SetBackdropBorderColor(t.checkFill[1], t.checkFill[2], t.checkFill[3], 1) + else + box:SetBackdropColor(t.checkBg[1], t.checkBg[2], t.checkBg[3], t.checkBg[4]) + box:SetBackdropBorderColor(t.checkBorder[1], t.checkBorder[2], t.checkBorder[3], t.checkBorder[4]) + end + end + + local txt = _G[name.."Text"] + if txt then + txt:ClearAllPoints() + txt:SetPoint("LEFT", cb, "RIGHT", 4, 0) + txt:SetFont(SFrames:GetFont(), 11, "OUTLINE") + txt:SetText(label) + txt:SetTextColor(t.text[1], t.text[2], t.text[3]) + end + + cb:SetScript("OnEnter", function() + if not this._chk then + box:SetBackdropBorderColor(t.checkHoverBorder[1], t.checkHoverBorder[2], t.checkHoverBorder[3], t.checkHoverBorder[4]) + end + end) + cb:SetScript("OnLeave", function() + if not this._chk then + box:SetBackdropBorderColor(t.checkBorder[1], t.checkBorder[2], t.checkBorder[3], t.checkBorder[4]) + end + end) + cb:SetScript("OnClick", function() + local v = (this:GetChecked() == 1 or this:GetChecked() == true) and true or false + this._chk = v; ApplyVis(v) + setter(v) + if onChange then onChange(v) end + PlaySound(v and "igMainMenuOptionCheckBoxOn" or "igMainMenuOptionCheckBoxOff") + end) + cb.Refresh = function() + local v = getter() and true or false + cb:SetChecked(v and 1 or 0); cb._chk = v; ApplyVis(v) + end + cb:Refresh() + return cb +end + +local function MakeSlider(parent, label, x, y, w, lo, hi, step, getter, setter, fmt) + local sn = WN("Sld") + local s = CreateFrame("Slider", sn, parent, "OptionsSliderTemplate") + s:SetWidth(w); s:SetHeight(20) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + s:SetMinMaxValues(lo, hi); s:SetValueStep(step) + if s.SetObeyStepOnDrag then s:SetObeyStepOnDrag(true) end + + local low = _G[sn.."Low"]; if low then low:SetText(tostring(lo)) end + local high = _G[sn.."High"]; if high then high:SetText(tostring(hi)) end + local text = _G[sn.."Text"] + + local t = T() + -- style track + local regions = { s:GetRegions() } + for i = 1, table.getn(regions) do + local r = regions[i] + if r and r.GetObjectType and r:GetObjectType() == "Texture" then r:SetTexture(nil) end + end + local track = s:CreateTexture(nil, "BACKGROUND") + track:SetTexture("Interface\\Buttons\\WHITE8X8") + track:SetPoint("LEFT", s, "LEFT", 0, 0); track:SetPoint("RIGHT", s, "RIGHT", 0, 0) + track:SetHeight(4) + track:SetVertexColor(t.sliderTrack[1], t.sliderTrack[2], t.sliderTrack[3], t.sliderTrack[4]) + local fill = s:CreateTexture(nil, "ARTWORK") + fill:SetTexture("Interface\\Buttons\\WHITE8X8") + fill:SetPoint("LEFT", track, "LEFT", 0, 0) + fill:SetPoint("TOP", track, "TOP", 0, 0); fill:SetPoint("BOTTOM", track, "BOTTOM", 0, 0) + fill:SetWidth(1) + fill:SetVertexColor(t.sliderFill[1], t.sliderFill[2], t.sliderFill[3], t.sliderFill[4]) + if s.SetThumbTexture then s:SetThumbTexture("Interface\\Buttons\\WHITE8X8") end + local thumb = s.GetThumbTexture and s:GetThumbTexture() + if thumb then thumb:SetWidth(8); thumb:SetHeight(14) + thumb:SetVertexColor(t.sliderThumb[1], t.sliderThumb[2], t.sliderThumb[3], t.sliderThumb[4]) + end + + local block = false + local function UpdateFill() + local mn, mx = s:GetMinMaxValues(); local v = s:GetValue() or mn + local pct = 0; if mx > mn then pct = Clamp((v-mn)/(mx-mn), 0, 1) end + local pw = math.floor((s:GetWidth() or 1) * pct + 0.5); if pw < 1 then pw = 1 end + fill:SetWidth(pw) + end + local function UpdateLabel(v) + local d = fmt and fmt(v) or v + if text then text:SetText(label..": "..tostring(d)) end + end + s:SetScript("OnValueChanged", function() + if block then return end + local raw = this:GetValue() or lo + local v; if step >= 1 then v = math.floor(raw+0.5) else v = math.floor(raw/step+0.5)*step end + v = Clamp(v, lo, hi); UpdateLabel(v); UpdateFill(); setter(v) + end) + s.Refresh = function() + local v = Clamp(tonumber(getter()) or lo, lo, hi) + block = true; s:SetValue(v); block = false; UpdateLabel(v); UpdateFill() + end + if low then low:SetTextColor(t.dimText[1], t.dimText[2], t.dimText[3]) + low:ClearAllPoints(); low:SetPoint("TOPLEFT", s, "BOTTOMLEFT", 0, 0) end + if high then high:SetTextColor(t.dimText[1], t.dimText[2], t.dimText[3]) + high:ClearAllPoints(); high:SetPoint("TOPRIGHT", s, "BOTTOMRIGHT", 0, 0) end + if text then text:SetTextColor(t.text[1], t.text[2], t.text[3]) + text:ClearAllPoints(); text:SetPoint("BOTTOM", s, "TOP", 0, 2) end + s:Refresh() + return s +end + +local function MakeLabel(parent, text, x, y, size, r, g, b) + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont(SFrames:GetFont(), size or 11, "OUTLINE") + fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + fs:SetText(text); fs:SetTextColor(r or 1, g or 1, b or 1) + return fs +end + +local function MakeDesc(parent, text, x, y, maxW) + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont(SFrames:GetFont(), 9, "OUTLINE") + fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + if maxW then fs:SetWidth(maxW); fs:SetJustifyH("LEFT") end + fs:SetText(text); fs:SetTextColor(0.65, 0.58, 0.62) + return fs +end + +local function MakeSection(parent, title, x, y, w, h, iconKey) + local sec = CreateFrame("Frame", WN("Sec"), parent) + sec:SetWidth(w); sec:SetHeight(h) + sec:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + sec:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + local t = T() + sec:SetBackdropColor(t.sectionBg[1], t.sectionBg[2], t.sectionBg[3], t.sectionBg[4]) + sec:SetBackdropBorderColor(t.sectionBorder[1], t.sectionBorder[2], t.sectionBorder[3], t.sectionBorder[4]) + + local anchorX = 10 + if iconKey and SFrames and SFrames.CreateIcon then + local ico = SFrames:CreateIcon(sec, iconKey, 12) + ico:SetDrawLayer("OVERLAY") + ico:SetPoint("TOPLEFT", sec, "TOPLEFT", 10, -9) + ico:SetVertexColor(t.title[1], t.title[2], t.title[3]) + anchorX = 26 + end + + local tfs = sec:CreateFontString(nil, "OVERLAY") + tfs:SetFont(SFrames:GetFont(), 11, "OUTLINE") + tfs:SetPoint("TOPLEFT", sec, "TOPLEFT", anchorX, -9) + tfs:SetText(title); tfs:SetTextColor(t.title[1], t.title[2], t.title[3]) + local div = sec:CreateTexture(nil, "ARTWORK") + div:SetTexture("Interface\\Buttons\\WHITE8X8"); div:SetHeight(1) + div:SetPoint("TOPLEFT", sec, "TOPLEFT", 8, -24) + div:SetPoint("TOPRIGHT", sec, "TOPRIGHT", -8, -24) + div:SetVertexColor(t.sectionBorder[1], t.sectionBorder[2], t.sectionBorder[3], 0.6) + return sec +end + +-- Toggle-group: row of small buttons, one active at a time +local function MakeBtnGroup(parent, x, y, opts, getter, setter) + local btns = {} + local bx = x + for _, o in ipairs(opts) do + local b = MakeButton(parent, o.label, o.w or 60, 22, nil) + b:ClearAllPoints(); b:SetPoint("TOPLEFT", parent, "TOPLEFT", bx, y) + b.key = o.key + b:SetScript("OnClick", function() + setter(this.key) + for _, bb in ipairs(btns) do bb.sfActive = (bb.key == this.key); bb:RefreshVisual() end + PlaySound("igMainMenuOptionCheckBoxOn") + end) + table.insert(btns, b); bx = bx + (o.w or 60) + 4 + end + local grp = {} + grp.Refresh = function() + local v = getter() + for _, bb in ipairs(btns) do bb.sfActive = (bb.key == v); bb:RefreshVisual() end + end + grp.Refresh() + return grp +end + +-------------------------------------------------------------------------------- +-- Default / current choices +-------------------------------------------------------------------------------- + +local function GetDefaultChoices() + return { + themePreset = "pink", + useClassTheme = false, + enableMerchant = true, + enableQuestUI = true, + enableQuestLogSkin = true, + enableTrainer = true, + enableSpellBook = true, + enableTradeSkill = true, + charPanelEnable = true, + enableMail = true, + enablePetStable = true, + enableSocial = true, + enableInspect = true, + enableFlightMap = true, + enableChat = true, + translateEnabled = false, + enableUnitFrames = true, + enableActionBars = true, + showPetBar = true, + showStanceBar = true, + showRightBars = true, + buttonRounded = false, + buttonInnerShadow = false, + minimapEnabled = true, + minimapShowClock = true, + minimapShowCoords = true, + minimapMapStyle = "auto", + buffEnabled = true, + buffIconSize = 30, + buffIconsPerRow = 8, + buffGrowDir = "LEFT", + buffShowTimer = true, + afkEnabled = true, + afkDelay = 5, + afkOutsideRest = false, + mapRevealEnabled = true, + mapRevealAlpha = 0.7, + worldMapEnabled = true, + hcGlobalDisable = true, + iconSet = "icon", + } +end + +local function GetCurrentChoices() + local c = GetDefaultChoices() + if not SFramesDB then return c end + local db = SFramesDB + if db.Theme then + if db.Theme.preset ~= nil then c.themePreset = db.Theme.preset end + if db.Theme.useClassTheme ~= nil then c.useClassTheme = db.Theme.useClassTheme end + if db.Theme.iconSet ~= nil then c.iconSet = db.Theme.iconSet end + end + if db.enableMerchant ~= nil then c.enableMerchant = db.enableMerchant end + if db.enableQuestUI ~= nil then c.enableQuestUI = db.enableQuestUI end + if db.enableQuestLogSkin ~= nil then c.enableQuestLogSkin = db.enableQuestLogSkin end + if db.enableTrainer ~= nil then c.enableTrainer = db.enableTrainer end + if db.enableSpellBook ~= nil then c.enableSpellBook = db.enableSpellBook end + if db.enableTradeSkill ~= nil then c.enableTradeSkill = db.enableTradeSkill end + if db.charPanelEnable ~= nil then c.charPanelEnable = db.charPanelEnable end + if db.enableMail ~= nil then c.enableMail = db.enableMail end + if db.enablePetStable ~= nil then c.enablePetStable = db.enablePetStable end + if db.enableSocial ~= nil then c.enableSocial = db.enableSocial end + if db.enableInspect ~= nil then c.enableInspect = db.enableInspect end + if db.enableFlightMap ~= nil then c.enableFlightMap = db.enableFlightMap end + if db.enableChat ~= nil then c.enableChat = db.enableChat end + if db.enableUnitFrames ~= nil then c.enableUnitFrames = db.enableUnitFrames end + if db.afkEnabled ~= nil then c.afkEnabled = db.afkEnabled end + if type(db.afkDelay) == "number" then c.afkDelay = db.afkDelay end + if db.afkOutsideRest ~= nil then c.afkOutsideRest = db.afkOutsideRest end + if db.Chat and db.Chat.translateEnabled ~= nil then c.translateEnabled = db.Chat.translateEnabled end + if db.Chat and db.Chat.hcGlobalDisable ~= nil then c.hcGlobalDisable = db.Chat.hcGlobalDisable end + if db.ActionBars then + if db.ActionBars.enable ~= nil then c.enableActionBars = db.ActionBars.enable end + if db.ActionBars.showPetBar ~= nil then c.showPetBar = db.ActionBars.showPetBar end + if db.ActionBars.showStanceBar ~= nil then c.showStanceBar = db.ActionBars.showStanceBar end + if db.ActionBars.showRightBars ~= nil then c.showRightBars = db.ActionBars.showRightBars end + if db.ActionBars.buttonRounded ~= nil then c.buttonRounded = db.ActionBars.buttonRounded end + if db.ActionBars.buttonInnerShadow ~= nil then c.buttonInnerShadow = db.ActionBars.buttonInnerShadow end + end + if db.Minimap then + if db.Minimap.enabled ~= nil then c.minimapEnabled = db.Minimap.enabled end + if db.Minimap.showClock ~= nil then c.minimapShowClock = db.Minimap.showClock end + if db.Minimap.showCoords ~= nil then c.minimapShowCoords = db.Minimap.showCoords end + if db.Minimap.mapStyle ~= nil then c.minimapMapStyle = db.Minimap.mapStyle end + end + if db.MinimapBuffs then + if db.MinimapBuffs.enabled ~= nil then c.buffEnabled = db.MinimapBuffs.enabled end + if type(db.MinimapBuffs.iconSize) == "number" then c.buffIconSize = db.MinimapBuffs.iconSize end + if type(db.MinimapBuffs.iconsPerRow) == "number" then c.buffIconsPerRow = db.MinimapBuffs.iconsPerRow end + if db.MinimapBuffs.growDirection ~= nil then c.buffGrowDir = db.MinimapBuffs.growDirection end + if db.MinimapBuffs.showTimer ~= nil then c.buffShowTimer = db.MinimapBuffs.showTimer end + end + if db.MapReveal then + if db.MapReveal.enabled ~= nil then c.mapRevealEnabled = db.MapReveal.enabled end + if type(db.MapReveal.unexploredAlpha) == "number" then c.mapRevealAlpha = db.MapReveal.unexploredAlpha end + end + if db.WorldMap then + if db.WorldMap.enabled ~= nil then c.worldMapEnabled = db.WorldMap.enabled end + end + return c +end + +-------------------------------------------------------------------------------- +-- Apply choices → SFramesDB +-------------------------------------------------------------------------------- + +local function ApplyChoices() + if not SFramesDB then SFramesDB = {} end + local c = choices + + if type(SFramesDB.Theme) ~= "table" then SFramesDB.Theme = {} end + SFramesDB.Theme.preset = c.themePreset + SFramesDB.Theme.useClassTheme = c.useClassTheme + SFramesDB.Theme.iconSet = c.iconSet or "icon" + + SFramesDB.enableMerchant = c.enableMerchant + SFramesDB.enableQuestUI = c.enableQuestUI + SFramesDB.enableQuestLogSkin = c.enableQuestLogSkin + SFramesDB.enableTrainer = c.enableTrainer + SFramesDB.enableSpellBook = c.enableSpellBook + SFramesDB.enableTradeSkill = c.enableTradeSkill + SFramesDB.charPanelEnable = c.charPanelEnable + SFramesDB.enableMail = c.enableMail + SFramesDB.enablePetStable = c.enablePetStable + SFramesDB.enableSocial = c.enableSocial + SFramesDB.enableInspect = c.enableInspect + SFramesDB.enableFlightMap = c.enableFlightMap + + SFramesDB.enableChat = c.enableChat + if type(SFramesDB.Chat) ~= "table" then SFramesDB.Chat = {} end + SFramesDB.Chat.translateEnabled = c.translateEnabled + SFramesDB.Chat.hcGlobalDisable = c.hcGlobalDisable + + SFramesDB.enableUnitFrames = c.enableUnitFrames + + if type(SFramesDB.ActionBars) ~= "table" then SFramesDB.ActionBars = {} end + SFramesDB.ActionBars.enable = c.enableActionBars + SFramesDB.ActionBars.showPetBar = c.showPetBar + SFramesDB.ActionBars.showStanceBar = c.showStanceBar + SFramesDB.ActionBars.showRightBars = c.showRightBars + SFramesDB.ActionBars.buttonRounded = c.buttonRounded + SFramesDB.ActionBars.buttonInnerShadow = c.buttonInnerShadow + + if type(SFramesDB.Minimap) ~= "table" then SFramesDB.Minimap = {} end + SFramesDB.Minimap.enabled = c.minimapEnabled + SFramesDB.Minimap.showClock = c.minimapShowClock + SFramesDB.Minimap.showCoords = c.minimapShowCoords + SFramesDB.Minimap.mapStyle = c.minimapMapStyle + + if type(SFramesDB.MinimapBuffs) ~= "table" then SFramesDB.MinimapBuffs = {} end + SFramesDB.MinimapBuffs.enabled = c.buffEnabled + SFramesDB.MinimapBuffs.iconSize = c.buffIconSize + SFramesDB.MinimapBuffs.iconsPerRow = c.buffIconsPerRow + SFramesDB.MinimapBuffs.growDirection = c.buffGrowDir + SFramesDB.MinimapBuffs.showTimer = c.buffShowTimer + + SFramesDB.afkEnabled = c.afkEnabled + SFramesDB.afkDelay = c.afkDelay + SFramesDB.afkOutsideRest = c.afkOutsideRest + + if type(SFramesDB.MapReveal) ~= "table" then SFramesDB.MapReveal = {} end + SFramesDB.MapReveal.enabled = c.mapRevealEnabled + SFramesDB.MapReveal.unexploredAlpha = c.mapRevealAlpha + + if type(SFramesDB.WorldMap) ~= "table" then SFramesDB.WorldMap = {} end + SFramesDB.WorldMap.enabled = c.worldMapEnabled + + SFrames.Theme:Apply(c.useClassTheme and SFrames.Theme:GetCurrentPreset() or c.themePreset) + SFramesDB.setupComplete = true +end + +-------------------------------------------------------------------------------- +-- Step builders (lazy — only called first time user navigates to that step) +-------------------------------------------------------------------------------- + +-- Step 1: Welcome ----------------------------------------------------------- +local function BuildWelcome(page) + local t = T() + local font = SFrames:GetFont() + + local logoIco = SFrames:CreateIcon(page, "logo", 52) + logoIco:SetDrawLayer("OVERLAY") + logoIco:SetPoint("TOP", page, "TOP", 0, -44) + + local logo = page:CreateFontString(nil, "OVERLAY") + logo:SetFont(font, 22, "OUTLINE") + logo:SetPoint("TOP", logoIco, "BOTTOM", 0, -8) + logo:SetText("Nanami-UI") + logo:SetTextColor(t.accentLight[1], t.accentLight[2], t.accentLight[3]) + + local sub = page:CreateFontString(nil, "OVERLAY") + sub:SetFont(font, 11, "OUTLINE") + sub:SetPoint("TOP", logo, "BOTTOM", 0, -4) + sub:SetText("v1.0.0") + sub:SetTextColor(t.dimText[1], t.dimText[2], t.dimText[3]) + + local tagline = page:CreateFontString(nil, "OVERLAY") + tagline:SetFont(font, 10, "OUTLINE") + tagline:SetPoint("TOP", sub, "BOTTOM", 0, -14) + tagline:SetWidth(440); tagline:SetJustifyH("CENTER") + tagline:SetText("一站式现代 UI 重塑方案,为经典怀旧而生") + tagline:SetTextColor(t.accentLight[1], t.accentLight[2], t.accentLight[3]) + + local desc = page:CreateFontString(nil, "OVERLAY") + desc:SetFont(font, 9.5, "OUTLINE") + desc:SetPoint("TOP", tagline, "BOTTOM", 0, -12) + desc:SetWidth(440); desc:SetJustifyH("CENTER") + desc:SetText( + "9 套可选主题配色 + 职业自适应配色\n".. + "全面重设计:商人 / 任务 / 技能书 / 邮箱 / 社交 等 12+ 界面\n".. + "现代化单位框体、动作条、聊天框与 Buff 栏\n".. + "AI 翻译、飞行助手、地图增强、AFK 待机动画\n".. + "轻量高效,所有功能均可自由开关\n\n".. + "接下来将引导你完成基础配置,\n".. + "你也可以随时跳过,直接使用默认设置。" + ) + desc:SetTextColor(0.78, 0.72, 0.76) + + local startBtn = MakeButton(page, "开始配置", 160, 32, function() + SW:GoTo(2) + end) + startBtn:SetPoint("TOP", desc, "BOTTOM", 0, -18) + + local skipBtn = MakeButton(page, "跳过,使用默认配置", 200, 28, function() + SW:DoSkip() + end) + skipBtn:SetPoint("TOP", startBtn, "BOTTOM", 0, -10) + + page:SetHeight(CONTENT_H) +end + +-- Step 2: Theme & Skins ---------------------------------------------------- +local function BuildTheme(page) + local t = T() + local font = SFrames:GetFont() + + local sec1 = MakeSection(page, "主题配色", 0, 0, CONTENT_W, 100, "star") + local presets = SFrames.Theme.PresetOrder + local swatches = {} + for idx = 1, table.getn(presets) do + local key = presets[idx] + local p = SFrames.Theme.Presets[key] + local sw = CreateFrame("Button", WN("Sw"), sec1) + sw:SetWidth(28); sw:SetHeight(28) + sw:SetPoint("TOPLEFT", sec1, "TOPLEFT", 14 + (idx-1)*34, -34) + sw:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = false, edgeSize = 8, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + local hr, hg, hb = SFrames.Theme.HSVtoRGB(p.hue, 0.40 * (p.satMul or 1), 0.80) + sw:SetBackdropColor(hr, hg, hb, 1) + sw.presetKey = key + sw:SetScript("OnClick", function() + choices.themePreset = this.presetKey + choices.useClassTheme = false + SFrames.Theme:Apply(this.presetKey) + SW:RefreshPanelColors() + for _, s in ipairs(swatches) do + if s.presetKey == this.presetKey then + s:SetBackdropBorderColor(1,1,1,1) + else + s:SetBackdropBorderColor(0.25,0.25,0.25,1) + end + end + if page._classCheck then page._classCheck:Refresh() end + PlaySound("igMainMenuOptionCheckBoxOn") + end) + local active = choices.themePreset + if key == active and not choices.useClassTheme then + sw:SetBackdropBorderColor(1,1,1,1) + else + sw:SetBackdropBorderColor(0.25,0.25,0.25,1) + end + sw:SetScript("OnEnter", function() + this:SetBackdropBorderColor(0.8,0.8,0.8,1) + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:SetText(SFrames.Theme.Presets[this.presetKey].name) + GameTooltip:Show() + end) + sw:SetScript("OnLeave", function() + local act = choices.themePreset + if this.presetKey == act and not choices.useClassTheme then + this:SetBackdropBorderColor(1,1,1,1) + else + this:SetBackdropBorderColor(0.25,0.25,0.25,1) + end + GameTooltip:Hide() + end) + table.insert(swatches, sw) + end + page._classCheck = MakeCheck(sec1, "根据当前职业自动选择主题色", 14, -72, + function() return choices.useClassTheme end, + function(v) choices.useClassTheme = v + if v then + SFrames.Theme:Apply(SFrames.Theme:GetCurrentPreset()) + else + SFrames.Theme:Apply(choices.themePreset) + end + SW:RefreshPanelColors() + end) + + -- Icon set picker (right after theme color) + local secIcon = MakeSection(page, "图标风格", 0, -108, CONTENT_W, 90, "star") + MakeDesc(secIcon, "选择图标风格(8 套可选,重载 UI 后生效)", 14, -28, CONTENT_W - 30) + + local ISET_NAMES = { "icon", "icon2", "icon3", "icon4", "icon5", "icon6", "icon7", "icon8" } + local ISET_SIZE = 36 + local ISET_GAP = 8 + local wFaction = UnitFactionGroup and UnitFactionGroup("player") or "Alliance" + local wFKey = (wFaction == "Horde") and "horde" or "alliance" + local wFCoords = SFrames.ICON_TCOORDS and SFrames.ICON_TCOORDS[wFKey] + if not wFCoords then + wFCoords = (wFKey == "horde") and { 0.75, 0.875, 0, 0.125 } or { 0.625, 0.75, 0, 0.125 } + end + + local isetBtns = {} + for idx = 1, table.getn(ISET_NAMES) do + local setKey = ISET_NAMES[idx] + local texPath = "Interface\\AddOns\\Nanami-UI\\img\\" .. setKey + local sx = 14 + (idx - 1) * (ISET_SIZE + ISET_GAP) + + local ib = CreateFrame("Button", WN("IS"), secIcon) + ib:SetWidth(ISET_SIZE); ib:SetHeight(ISET_SIZE) + ib:SetPoint("TOPLEFT", secIcon, "TOPLEFT", sx, -44) + ib:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + ib:SetBackdropColor(0.08, 0.08, 0.1, 0.85) + ib:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + + local pv = ib:CreateTexture(nil, "ARTWORK") + pv:SetTexture(texPath) + pv:SetTexCoord(wFCoords[1], wFCoords[2], wFCoords[3], wFCoords[4]) + pv:SetPoint("CENTER", ib, "CENTER", 0, 0) + pv:SetWidth(ISET_SIZE - 6); pv:SetHeight(ISET_SIZE - 6) + + ib.setKey = setKey + ib:SetScript("OnClick", function() + choices.iconSet = this.setKey + for _, b in ipairs(isetBtns) do + if b.setKey == this.setKey then + b:SetBackdropBorderColor(1, 0.85, 0.6, 1) + else + b:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + end + end + PlaySound("igMainMenuOptionCheckBoxOn") + end) + ib:SetScript("OnEnter", function() + this:SetBackdropBorderColor(0.7, 0.7, 0.7, 1) + GameTooltip:SetOwner(this, "ANCHOR_NONE") + GameTooltip:ClearAllPoints() + GameTooltip:SetPoint("BOTTOM", this, "TOP", 0, 6) + local num = this.setKey == "icon" and "1" or string.sub(this.setKey, 5) + GameTooltip:SetText("图标风格 " .. num .. (this.setKey == "icon" and " (默认)" or "")) + GameTooltip:Show() + GameTooltip:SetFrameStrata("TOOLTIP") + GameTooltip:Raise() + end) + ib:SetScript("OnLeave", function() + local cur = choices.iconSet or "icon" + if this.setKey == cur then + this:SetBackdropBorderColor(1, 0.85, 0.6, 1) + else + this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + end + GameTooltip:Hide() + end) + + local cur = choices.iconSet or "icon" + if setKey == cur then + ib:SetBackdropBorderColor(1, 0.85, 0.6, 1) + end + table.insert(isetBtns, ib) + end + + -- Skin toggles (below icon picker) + local sec2 = MakeSection(page, "界面皮肤替换", 0, -206, CONTENT_W, 150, "settings") + MakeDesc(sec2, "选择启用哪些 Nanami-UI 美化界面(替换原生窗口)", 14, -28, CONTENT_W - 30) + + local skins = { + { key = "enableMerchant", label = "商人界面" }, + { key = "enableQuestUI", label = "任务/NPC 对话" }, + { key = "enableQuestLogSkin",label = "任务日志" }, + { key = "enableSpellBook", label = "技能书" }, + { key = "enableTradeSkill", label = "专业技能" }, + { key = "enableTrainer", label = "训练师" }, + { key = "charPanelEnable", label = "人物面板" }, + { key = "enableMail", label = "邮箱" }, + { key = "enablePetStable", label = "兽栏" }, + { key = "enableSocial", label = "社交" }, + { key = "enableInspect", label = "观察面板" }, + { key = "enableFlightMap", label = "飞行地图" }, + } + local col, row = 0, 0 + for i = 1, table.getn(skins) do + local sk = skins[i] + local cx = 14 + col * 180 + local cy = -44 - row * 22 + MakeCheck(sec2, sk.label, cx, cy, + function() return choices[sk.key] end, + function(v) choices[sk.key] = v end) + col = col + 1 + if col >= 3 then col = 0; row = row + 1 end + end + + page:SetHeight(370) +end + +-- Step 3: Chat & Translation ------------------------------------------------ +local function BuildChat(page) + local t = T() + local sec1 = MakeSection(page, "聊天框", 0, 0, CONTENT_W, 80, "chat") + + local chatCb = MakeCheck(sec1, "启用 Nanami-UI 聊天框加强", 14, -34, + function() return choices.enableChat end, + function(v) choices.enableChat = v end) + MakeDesc(sec1, "标签式多频道、频道过滤、样式美化、功能增强", 36, -52, CONTENT_W - 50) + page._chatCb = chatCb + + local sec2 = MakeSection(page, "AI 翻译", 0, -90, CONTENT_W, 150, "ai") + + local transCb = MakeCheck(sec2, "启用聊天频道 AI 自动翻译", 14, -34, + function() return choices.translateEnabled end, + function(v) + choices.translateEnabled = v + if v then + choices.enableChat = true + if page._chatCb then page._chatCb:Refresh() end + end + end) + + MakeDesc(sec2, + "需要 STranslate 外部程序和 API 支持。\n若不使用翻译功能,STranslate 无需运行。\n\n启用翻译将自动启用聊天框加强。\n翻译频道的详细配置请在聊天设置中调整。", + 14, -56, CONTENT_W - 30) + + page:SetHeight(250) +end + +-- Step 4: Unit Frames + Action Bars (combined) ----------------------------- +local function BuildFramesAndBars(page) + -- Unit Frames section + local sec1 = MakeSection(page, "单位框体", 0, 0, CONTENT_W, 90, "character") + MakeCheck(sec1, "启用 Nanami-UI 单位框体", 14, -34, + function() return choices.enableUnitFrames end, + function(v) choices.enableUnitFrames = v end) + MakeDesc(sec1, + "替换原生玩家、目标、宠物、小队、团队框体为现代极简风格", + 14, -56, CONTENT_W - 30) + + -- Action Bars section + local sec2 = MakeSection(page, "动作条", 0, -100, CONTENT_W, 210, "attack") + local subFrame = CreateFrame("Frame", nil, sec2) + subFrame:SetPoint("TOPLEFT", sec2, "TOPLEFT", 0, -60) + subFrame:SetWidth(CONTENT_W); subFrame:SetHeight(140) + + MakeCheck(sec2, "启用 Nanami-UI 动作条接管", 14, -34, + function() return choices.enableActionBars end, + function(v) + choices.enableActionBars = v + if v then subFrame:Show() else subFrame:Hide() end + end) + MakeDesc(sec2, "需要 /reload 生效", 36, -52, 300) + + MakeCheck(subFrame, "显示宠物动作条", 30, -6, + function() return choices.showPetBar end, + function(v) choices.showPetBar = v end) + MakeCheck(subFrame, "显示姿态栏 / 形态栏", 30, -30, + function() return choices.showStanceBar end, + function(v) choices.showStanceBar = v end) + MakeCheck(subFrame, "显示右侧栏", 30, -54, + function() return choices.showRightBars end, + function(v) choices.showRightBars = v end) + MakeCheck(subFrame, "按钮圆角", 30, -78, + function() return choices.buttonRounded end, + function(v) choices.buttonRounded = v end) + MakeCheck(subFrame, "按钮内阴影", 230, -78, + function() return choices.buttonInnerShadow end, + function(v) choices.buttonInnerShadow = v end) + + if not choices.enableActionBars then subFrame:Hide() end + page:SetHeight(320) +end + +-- Step 6: Extras (with sub-options) ----------------------------------------- +local function BuildExtras(page) + local items = {} + local allControls = {} + + local function AddFeature(label, desc, enableKey, subBuilder, iconKey) + table.insert(items, { label = label, desc = desc, key = enableKey, subBuilder = subBuilder, icon = iconKey }) + end + + AddFeature("小地图美化", "自定义小地图外观", "minimapEnabled", function(p, x, y) + MakeCheck(p, "显示时钟", x, y, + function() return choices.minimapShowClock end, + function(v) choices.minimapShowClock = v end) + MakeCheck(p, "显示坐标", x + 160, y, + function() return choices.minimapShowCoords end, + function(v) choices.minimapShowCoords = v end) + MakeLabel(p, "地图风格:", x, y - 26, 10, 0.78, 0.72, 0.76) + MakeBtnGroup(p, x + 60, y - 24, + { {key="auto", label="自动", w=50}, {key="round", label="圆形", w=50}, {key="square", label="方形", w=50} }, + function() return choices.minimapMapStyle end, + function(v) choices.minimapMapStyle = v end) + return 54 + end, "worldmap") + + AddFeature("Buff 栏", "自定义 Buff/Debuff 显示", "buffEnabled", function(p, x, y) + MakeSlider(p, "图标大小", x, y - 10, 180, 16, 50, 1, + function() return choices.buffIconSize end, + function(v) choices.buffIconSize = v end) + MakeSlider(p, "每行数量", x + 220, y - 10, 180, 4, 16, 1, + function() return choices.buffIconsPerRow end, + function(v) choices.buffIconsPerRow = v end) + MakeLabel(p, "增长方向:", x, y - 52, 10, 0.78, 0.72, 0.76) + MakeBtnGroup(p, x + 60, y - 50, + { {key="LEFT", label="向左", w=52}, {key="RIGHT", label="向右", w=52} }, + function() return choices.buffGrowDir end, + function(v) choices.buffGrowDir = v end) + MakeCheck(p, "显示计时器", x + 220, y - 50, + function() return choices.buffShowTimer end, + function(v) choices.buffShowTimer = v end) + return 78 + end, "buff") + + AddFeature("AFK 待机动画", "进入暂离后显示全屏待机画面", "afkEnabled", function(p, x, y) + MakeSlider(p, "触发延迟(分钟)", x, y - 10, 200, 1, 30, 1, + function() return choices.afkDelay end, + function(v) choices.afkDelay = v end) + MakeCheck(p, "仅在休息区触发", x + 250, y - 4, + function() return choices.afkOutsideRest end, + function(v) choices.afkOutsideRest = v end) + return 48 + end, "camp") + + AddFeature("地图迷雾揭示", "显示未探索区域的轮廓", "mapRevealEnabled", function(p, x, y) + MakeSlider(p, "未探索区域透明度", x, y - 10, 240, 0, 1, 0.05, + function() return choices.mapRevealAlpha end, + function(v) choices.mapRevealAlpha = v end, + function(v) return string.format("%.0f%%", v * 100) end) + return 48 + end, "search") + + AddFeature("世界地图皮肤", "美化世界地图界面", "worldMapEnabled", nil, "worldmap") + AddFeature("飞行助手", "美化飞行界面 + 飞行进度条", "enableFlightMap", nil, "mount") + + -- Build layout + local yOff = -4 + for i = 1, table.getn(items) do + local item = items[i] + local sec = MakeSection(page, item.label, 0, yOff, CONTENT_W, 30, item.icon) + MakeDesc(sec, item.desc, 14, -30, CONTENT_W - 30) + sec:SetHeight(50) + + local subFrame = nil + local subH = 0 + + if item.subBuilder then + subFrame = CreateFrame("Frame", nil, page) + subFrame:SetWidth(CONTENT_W) + end + + local cb = MakeCheck(sec, "启用", CONTENT_W - 80, -8, + function() return choices[item.key] end, + function(v) + choices[item.key] = v + if subFrame then + if v then subFrame:Show() else subFrame:Hide() end + end + end) + + if subFrame then + subH = item.subBuilder(subFrame, 14, -4) or 40 + subFrame:SetHeight(subH) + subFrame:SetPoint("TOPLEFT", page, "TOPLEFT", 0, yOff - 50) + if not choices[item.key] then subFrame:Hide() end + yOff = yOff - 50 - subH - 6 + else + yOff = yOff - 56 + end + end + + page:SetHeight(math.abs(yOff) + 10) +end + +-- Step 7: Hardcore (conditional) -------------------------------------------- +local function BuildHardcore(page) + local sec = MakeSection(page, "硬核模式", 0, 0, CONTENT_W, 120, "skull") + MakeDesc(sec, "检测到你的角色处于硬核模式,建议启用硬核频道接收\n以获取硬核死亡消息通知。", 14, -30, CONTENT_W - 30) + + MakeCheck(sec, "启用硬核频道接收", 14, -68, + function() return not choices.hcGlobalDisable end, + function(v) choices.hcGlobalDisable = not v end) + MakeDesc(sec, "关闭将全局屏蔽硬核频道消息", 36, -86, 300) + + page:SetHeight(140) +end + +-- Complete page -------------------------------------------------------------- +local function BuildComplete(page) + local t = T() + local font = SFrames:GetFont() + + local doneIco = SFrames:CreateIcon(page, "logo", 48) + doneIco:SetDrawLayer("OVERLAY") + doneIco:SetPoint("TOP", page, "TOP", 0, -16) + doneIco:SetVertexColor(t.accentLight[1], t.accentLight[2], t.accentLight[3]) + + local icon = page:CreateFontString(nil, "OVERLAY") + icon:SetFont(font, 28, "OUTLINE") + icon:SetPoint("TOP", doneIco, "BOTTOM", 0, -4) + icon:SetText("=^_^=") + icon:SetTextColor(t.accentLight[1], t.accentLight[2], t.accentLight[3]) + + local title = page:CreateFontString(nil, "OVERLAY") + title:SetFont(font, 16, "OUTLINE") + title:SetPoint("TOP", icon, "BOTTOM", 0, -10) + title:SetText("配置完成!") + title:SetTextColor(t.title[1], t.title[2], t.title[3]) + + local desc = page:CreateFontString(nil, "OVERLAY") + desc:SetFont(font, 10, "OUTLINE") + desc:SetPoint("TOP", title, "BOTTOM", 0, -14) + desc:SetWidth(420); desc:SetJustifyH("CENTER") + desc:SetTextColor(0.78, 0.72, 0.76) + + if runMode == "rerun" then + desc:SetText("设置已更新。部分选项需要 /reload 才能完全生效。\n你可以随时通过 /nui config 或 ESC 菜单调整设置。") + else + desc:SetText("所有设置已保存。\n你可以随时通过 /nui config 或 ESC 菜单调整设置。") + end + + local doneBtn = MakeButton(page, "完成", 140, 32, function() + SW:DoComplete() + end) + doneBtn:SetPoint("TOP", desc, "BOTTOM", 0, -24) + + page:SetHeight(CONTENT_H) +end + +-------------------------------------------------------------------------------- +-- Build main frame (once) +-------------------------------------------------------------------------------- + +local function EnsureWizardFrame() + if built then return end + built = true + + local t = T() + + -- Full-screen overlay + overlay = CreateFrame("Frame", "NanamiWizOverlay", UIParent) + overlay:SetFrameStrata("DIALOG") + overlay:SetAllPoints(UIParent) + overlay:EnableMouse(true) + overlay:SetScript("OnMouseDown", function() end) + local bg = overlay:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints(overlay) + bg:SetTexture(0, 0, 0, 0.6) + + -- Center panel + panel = CreateFrame("Frame", "NanamiWizPanel", overlay) + panel:SetWidth(PANEL_W); panel:SetHeight(PANEL_H) + panel:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + panel:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + panel:SetBackdropColor(t.panelBg[1], t.panelBg[2], t.panelBg[3], t.panelBg[4]) + panel:SetBackdropBorderColor(t.panelBorder[1], t.panelBorder[2], t.panelBorder[3], t.panelBorder[4]) + + -- Header + headerTitle = panel:CreateFontString(nil, "OVERLAY") + headerTitle:SetFont(SFrames:GetFont(), 13, "OUTLINE") + headerTitle:SetPoint("TOP", panel, "TOP", 0, -14) + headerTitle:SetTextColor(t.title[1], t.title[2], t.title[3]) + + -- Step dots row + local dotHolder = CreateFrame("Frame", nil, panel) + dotHolder:SetWidth(PANEL_W); dotHolder:SetHeight(16) + dotHolder:SetPoint("TOP", panel, "TOP", 0, -34) + + -- Content scroll area + local contentHolder = CreateFrame("Frame", nil, panel) + contentHolder:SetWidth(CONTENT_W); contentHolder:SetHeight(CONTENT_H) + contentHolder:SetPoint("TOPLEFT", panel, "TOPLEFT", 20, -56) + + contentScroll = CreateFrame("ScrollFrame", WN("Scr"), contentHolder) + contentScroll:SetAllPoints(contentHolder) + + contentChild = CreateFrame("Frame", nil, contentScroll) + contentChild:SetWidth(CONTENT_W) + contentChild:SetHeight(CONTENT_H) + contentScroll:SetScrollChild(contentChild) + + contentHolder:EnableMouseWheel(true) + contentHolder:SetScript("OnMouseWheel", function() + local cur = contentScroll:GetVerticalScroll() + local maxS = math.max(0, contentChild:GetHeight() - CONTENT_H) + local nv = cur - arg1 * 30 + if nv < 0 then nv = 0 end + if nv > maxS then nv = maxS end + contentScroll:SetVerticalScroll(nv) + end) + + -- Footer navigation + local footer = CreateFrame("Frame", nil, panel) + footer:SetWidth(PANEL_W - 40); footer:SetHeight(36) + footer:SetPoint("BOTTOM", panel, "BOTTOM", 0, 12) + + btnPrev = MakeButton(footer, "< 上一步", 120, 28, function() SW:GoPrev() end) + btnPrev:SetPoint("LEFT", footer, "LEFT", 0, 0) + + stepLabel = footer:CreateFontString(nil, "OVERLAY") + stepLabel:SetFont(SFrames:GetFont(), 10, "OUTLINE") + stepLabel:SetPoint("CENTER", footer, "CENTER", 0, 0) + stepLabel:SetTextColor(t.dimText[1], t.dimText[2], t.dimText[3]) + + btnNext = MakeButton(footer, "下一步 >", 120, 28, function() SW:GoNext() end) + btnNext:SetPoint("RIGHT", footer, "RIGHT", 0, 0) + + btnSkip = CreateFrame("Button", WN("Skip"), footer) + btnSkip:SetWidth(60); btnSkip:SetHeight(16) + btnSkip:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -14, 4) + local skipFs = btnSkip:CreateFontString(nil, "OVERLAY") + skipFs:SetFont(SFrames:GetFont(), 9, "OUTLINE") + skipFs:SetAllPoints(btnSkip) + skipFs:SetText("跳过全部") + skipFs:SetTextColor(0.55, 0.50, 0.54) + btnSkip:SetScript("OnClick", function() SW:DoSkip() end) + btnSkip:SetScript("OnEnter", function() skipFs:SetTextColor(0.85, 0.75, 0.80) end) + btnSkip:SetScript("OnLeave", function() skipFs:SetTextColor(0.55, 0.50, 0.54) end) + + -- Store dot holder for later + panel._dotHolder = dotHolder +end + +-------------------------------------------------------------------------------- +-- Step list & dot creation +-------------------------------------------------------------------------------- + +local function BuildStepList() + stepList = {} + table.insert(stepList, { id = "welcome", title = "欢迎", builder = BuildWelcome }) + table.insert(stepList, { id = "theme", title = "主题与皮肤", builder = BuildTheme }) + table.insert(stepList, { id = "chat", title = "聊天与翻译", builder = BuildChat }) + table.insert(stepList, { id = "framesbars", title = "框体与动作条", builder = BuildFramesAndBars }) + table.insert(stepList, { id = "extras", title = "辅助功能", builder = BuildExtras }) + + -- Hardcore: only if player is hardcore + local isHC = false + if C_TurtleWoW and C_TurtleWoW.IsHardcore then + local ok, val = pcall(C_TurtleWoW.IsHardcore, "player") + if ok and val then isHC = true end + end + if isHC then + table.insert(stepList, { id = "hardcore", title = "硬核模式", builder = BuildHardcore }) + end + + table.insert(stepList, { id = "complete", title = "完成", builder = BuildComplete }) + + -- Create step pages (empty frames, lazily populated) + for _, pg in ipairs(stepPages) do if pg then pg:Hide() end end + stepPages = {} + for i = 1, table.getn(stepList) do + local pg = CreateFrame("Frame", nil, contentChild) + pg:SetWidth(CONTENT_W) + pg:SetHeight(CONTENT_H) + pg:SetPoint("TOPLEFT", contentChild, "TOPLEFT", 0, 0) + pg:Hide() + pg._built = false + stepPages[i] = pg + end + + -- Create dots + for _, d in ipairs(stepDots) do if d then d:Hide() end end + stepDots = {} + local totalDots = table.getn(stepList) + local dotSpacing = 18 + local startX = (PANEL_W - totalDots * dotSpacing) / 2 + for i = 1, totalDots do + local dot = CreateFrame("Frame", nil, panel._dotHolder) + dot:SetWidth(8); dot:SetHeight(8) + dot:SetPoint("TOPLEFT", panel._dotHolder, "TOPLEFT", startX + (i-1)*dotSpacing, -4) + dot:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, edgeSize = 1, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + stepDots[i] = dot + end +end + +-------------------------------------------------------------------------------- +-- Navigation +-------------------------------------------------------------------------------- + +function SW:RefreshPanelColors() + if not panel then return end + local t = T() + panel:SetBackdropColor(t.panelBg[1], t.panelBg[2], t.panelBg[3], t.panelBg[4]) + panel:SetBackdropBorderColor(t.panelBorder[1], t.panelBorder[2], t.panelBorder[3], t.panelBorder[4]) + if headerTitle then headerTitle:SetTextColor(t.title[1], t.title[2], t.title[3]) end +end + +function SW:GoTo(idx) + if idx < 1 or idx > table.getn(stepList) then return end + currentStep = idx + + -- Lazily build + local pg = stepPages[idx] + if not pg._built then + pg._built = true + local ok, err = pcall(stepList[idx].builder, pg) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] Wizard step error ("..stepList[idx].id.."): "..tostring(err).."|r") + end + end + + -- Show/hide pages + for i = 1, table.getn(stepPages) do + if i == idx then stepPages[i]:Show() else stepPages[i]:Hide() end + end + + -- Set scroll child height and reset scroll + contentChild:SetHeight(math.max(pg:GetHeight(), CONTENT_H)) + contentScroll:SetVerticalScroll(0) + + -- Update header + headerTitle:SetText(stepList[idx].title) + + -- Update dots + local t = T() + for i = 1, table.getn(stepDots) do + local d = stepDots[i] + if i == idx then + d:SetBackdropColor(t.accentLight[1], t.accentLight[2], t.accentLight[3], 1) + d:SetBackdropBorderColor(t.accentLight[1], t.accentLight[2], t.accentLight[3], 1) + elseif i < idx then + d:SetBackdropColor(t.accent[1], t.accent[2], t.accent[3], 0.5) + d:SetBackdropBorderColor(t.accent[1], t.accent[2], t.accent[3], 0.5) + else + d:SetBackdropColor(0.3, 0.3, 0.3, 0.6) + d:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.6) + end + end + + -- Update step label + stepLabel:SetText(idx .. " / " .. table.getn(stepList)) + + -- Navigation buttons + local isFirst = (idx == 1) + local isLast = (idx == table.getn(stepList)) + local isWelcome = (stepList[idx].id == "welcome") + local isDone = (stepList[idx].id == "complete") + + if isWelcome then + btnPrev:Hide(); btnNext:Hide(); btnSkip:Hide(); stepLabel:SetText("") + elseif isDone then + btnPrev:Show(); btnNext:Hide(); btnSkip:Hide() + else + btnPrev:Show(); btnNext:Show(); btnSkip:Show() + if isFirst then btnPrev:Hide() end + end +end + +function SW:GoNext() + if currentStep < table.getn(stepList) then + self:GoTo(currentStep + 1) + end +end + +function SW:GoPrev() + if currentStep > 1 then + self:GoTo(currentStep - 1) + end +end + +function SW:DoSkip() + if runMode == "rerun" then + self:Hide() + return + end + -- First-run: apply defaults + if not SFramesDB then SFramesDB = {} end + SFramesDB.setupComplete = true + self:Hide() + if completeCb then completeCb() end +end + +function SW:DoComplete() + local ok, err = pcall(ApplyChoices) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] Wizard apply error: "..tostring(err).."|r") + if not SFramesDB then SFramesDB = {} end + SFramesDB.setupComplete = true + end + self:Hide() + ReloadUI() +end + +-------------------------------------------------------------------------------- +-- Public API +-------------------------------------------------------------------------------- + +function SW:Show(callback, mode) + local ok, err = pcall(function() + runMode = mode or "firstrun" + completeCb = callback + + if runMode == "rerun" then + choices = GetCurrentChoices() + else + choices = GetDefaultChoices() + end + + EnsureWizardFrame() + BuildStepList() + + overlay:Show() + overlay:Raise() + + self:GoTo(1) + end) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] Setup wizard failed: "..tostring(err).."|r") + if not SFramesDB then SFramesDB = {} end + SFramesDB.setupComplete = true + if callback then callback() end + end +end + +function SW:Hide() + if overlay then overlay:Hide() end +end diff --git a/SocialUI.lua b/SocialUI.lua new file mode 100644 index 0000000..dac22c1 --- /dev/null +++ b/SocialUI.lua @@ -0,0 +1,2331 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Social UI (SocialUI.lua) +-- Replaces FriendsFrame with modern rounded UI +-- Tabs: Friends/Ignore, Who, Guild, Raid +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.SocialUI = {} +local SUI = SFrames.SocialUI +SFramesDB = SFramesDB or {} + +-------------------------------------------------------------------------------- +-- Theme (Pink Cat-Paw) +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + onlineText = { 0.30, 1.0, 0.30 }, + offlineText = { 0.50, 0.45, 0.48 }, +}) + +local CLASS_COLORS = { + ["WARRIOR"] = { 0.78, 0.61, 0.43 }, + ["MAGE"] = { 0.41, 0.80, 0.94 }, + ["ROGUE"] = { 1.00, 0.96, 0.41 }, + ["DRUID"] = { 1.00, 0.49, 0.04 }, + ["HUNTER"] = { 0.67, 0.83, 0.45 }, + ["SHAMAN"] = { 0.14, 0.35, 1.00 }, + ["PRIEST"] = { 1.00, 1.00, 1.00 }, + ["WARLOCK"] = { 0.58, 0.51, 0.79 }, + ["PALADIN"] = { 0.96, 0.55, 0.73 }, +} + +-- Reverse lookup: localized class name -> English key +local CLASS_NAME_TO_EN = {} +local function BuildClassReverseLookup() + -- Try WoW global tables first + if LOCALIZED_CLASS_NAMES_MALE then + for en, loc in pairs(LOCALIZED_CLASS_NAMES_MALE) do + CLASS_NAME_TO_EN[loc] = en + end + end + if LOCALIZED_CLASS_NAMES_FEMALE then + for en, loc in pairs(LOCALIZED_CLASS_NAMES_FEMALE) do + CLASS_NAME_TO_EN[loc] = en + end + end + -- Hardcoded Chinese fallback + local zhMap = { + ["战士"] = "WARRIOR", ["法师"] = "MAGE", ["盗贼"] = "ROGUE", + ["德鲁伊"] = "DRUID", ["猎人"] = "HUNTER", ["萨满祭司"] = "SHAMAN", + ["牧师"] = "PRIEST", ["术士"] = "WARLOCK", ["圣骑士"] = "PALADIN", + } + for loc, en in pairs(zhMap) do + if not CLASS_NAME_TO_EN[loc] then + CLASS_NAME_TO_EN[loc] = en + end + end + -- Also add English names themselves + for en, _ in pairs(CLASS_COLORS) do + CLASS_NAME_TO_EN[en] = en + CLASS_NAME_TO_EN[string.lower(en)] = en + end +end + +local CLASS_ICON_PATH = "Interface\\AddOns\\Nanami-UI\\img\\UI-Classes-Circles" +local CLASS_ICON_TCOORDS = { + ["WARRIOR"] = { 0, 0.25, 0, 0.25 }, + ["MAGE"] = { 0.25, 0.49609375, 0, 0.25 }, + ["ROGUE"] = { 0.49609375, 0.7421875, 0, 0.25 }, + ["DRUID"] = { 0.7421875, 0.98828125, 0, 0.25 }, + ["HUNTER"] = { 0, 0.25, 0.25, 0.5 }, + ["SHAMAN"] = { 0.25, 0.49609375, 0.25, 0.5 }, + ["PRIEST"] = { 0.49609375, 0.7421875, 0.25, 0.5 }, + ["WARLOCK"] = { 0.7421875, 0.98828125, 0.25, 0.5 }, + ["PALADIN"] = { 0, 0.25, 0.5, 0.75 }, +} + +-------------------------------------------------------------------------------- +-- Layout +-------------------------------------------------------------------------------- +local FRAME_W = 380 +local FRAME_H = 455 +local HEADER_H = 30 +local TAB_BAR_H = 26 +local SIDE_PAD = 10 +local CONTENT_W = FRAME_W - SIDE_PAD * 2 +local ROW_H = 22 +local BOTTOM_H = 30 +local SCROLL_STEP = 22 + +-------------------------------------------------------------------------------- +-- State +-------------------------------------------------------------------------------- +local MainFrame = nil +local mainTabs = {} +local pages = {} +local currentMainTab = 1 +local initialized = false + +local friendRows = {} +local ignoreRows = {} +local whoRows = {} +local guildRows = {} +local raidSlots = {} +local selectedFriend = nil +local selectedWho = nil +local selectedGuild = nil +local friendSubTab = "friends" +local guildViewMode = "player" +local guildHideOffline = false +local guildSortField = "name" +local guildSortAsc = true +local friendSearchText = "" +local guildSearchText = "" +local origShowFriendsAPI = nil + +local widgetId = 0 +local function NextName(p) + widgetId = widgetId + 1 + return "SFramesSocial" .. (p or "") .. tostring(widgetId) +end + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + local bg = bgColor or T.panelBg + local bd = borderColor or T.panelBorder + frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) + frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) +end + +local function SetPixelBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + local bg = bgColor or T.slotBg + local bd = borderColor or T.slotBorder + frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) + frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) +end + +local function SetRowNormal(frame) + SetPixelBackdrop(frame, T.rowNormal, T.rowNormalBd) +end + +local function AddSelHighlight(row) + local sb = row:CreateTexture(nil, "ARTWORK") + sb:SetTexture("Interface\\Buttons\\WHITE8X8") + sb:SetAllPoints(row) + sb:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.35) + sb:Hide(); row._selBg = sb + + local sg = row:CreateTexture(nil, "ARTWORK") + sg:SetTexture("Interface\\Buttons\\WHITE8X8") + sg:SetWidth(4) + sg:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0) + sg:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) + sg:SetVertexColor(1, 0.65, 0.85, 1) + sg:Hide(); row._selGlow = sg + + local st = row:CreateTexture(nil, "OVERLAY") + st:SetTexture("Interface\\Buttons\\WHITE8X8") + st:SetHeight(1) + st:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0) + st:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 0) + st:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.8) + st:Hide(); row._selTop = st + + local sbo = row:CreateTexture(nil, "OVERLAY") + sbo:SetTexture("Interface\\Buttons\\WHITE8X8") + sbo:SetHeight(1) + sbo:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) + sbo:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0) + sbo:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.8) + sbo:Hide(); row._selBot = sbo +end + +local function ShowSelHighlight(row) + if row._selBg then row._selBg:Show() end + if row._selGlow then row._selGlow:Show() end + if row._selTop then row._selTop:Show() end + if row._selBot then row._selBot:Show() end +end + +local function HideSelHighlight(row) + if row._selBg then row._selBg:Hide() end + if row._selGlow then row._selGlow:Hide() end + if row._selTop then row._selTop:Hide() end + if row._selBot then row._selBot:Hide() end +end + +local function CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0)) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.6) + s:SetBackdropBorderColor(0, 0, 0, 0.45) + return s +end + +local function MakeFS(parent, size, justifyH, color) + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), size or 11, "OUTLINE") + fs:SetJustifyH(justifyH or "LEFT") + local c = color or T.nameText + fs:SetTextColor(c[1], c[2], c[3]) + return fs +end + +local function MakeSep(parent, y) + local sep = parent:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + sep:SetHeight(1) + sep:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, y) + sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, y) + return sep +end + +local function MakeButton(parent, text, w, h) + local btn = CreateFrame("Button", NextName("Btn"), parent) + btn:SetWidth(w or 80) + btn:SetHeight(h or 22) + SetRoundBackdrop(btn, T.btnBg, T.btnBorder) + local fs = MakeFS(btn, 11, "CENTER", T.btnText) + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text or "") + btn.text = fs + btn:SetScript("OnEnter", function() + SetRoundBackdrop(this, T.btnHoverBg, T.tabActiveBorder) + this.text:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end) + btn:SetScript("OnLeave", function() + SetRoundBackdrop(this, T.btnBg, T.btnBorder) + this.text:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end) + return btn +end + +local function MakeEditBox(parent, w, h) + local box = CreateFrame("EditBox", NextName("Edit"), parent) + box:SetWidth(w or 200) + box:SetHeight(h or 22) + box:SetFont(GetFont(), 11, "OUTLINE") + box:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + box:SetAutoFocus(false) + box:SetMaxLetters(256) + SetPixelBackdrop(box, T.inputBg, T.inputBorder) + box:SetTextInsets(6, 6, 0, 0) + box:SetScript("OnEscapePressed", function() this:ClearFocus() end) + return box +end + +local customPopup = nil +local function ShowCustomPopup(title, hasInput, onAccept, onCancel) + if not customPopup then + local f = CreateFrame("Frame", "SFramesSocialPopup", UIParent) + f:SetWidth(280) + f:SetHeight(120) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 80) + f:SetFrameStrata("DIALOG") + f:SetFrameLevel(100) + SetRoundBackdrop(f, T.panelBg, T.panelBorder) + CreateShadow(f) + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + local titleFS = MakeFS(f, 12, "CENTER", T.headerText) + titleFS:SetPoint("TOP", f, "TOP", 0, -10) + f.titleFS = titleFS + + local editBox = CreateFrame("EditBox", "SFramesSocialPopupEdit", f) + editBox:SetWidth(230) + editBox:SetHeight(24) + editBox:SetPoint("TOP", titleFS, "BOTTOM", 0, -10) + editBox:SetFont(GetFont(), 11, "OUTLINE") + editBox:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + editBox:SetAutoFocus(false) + editBox:SetMaxLetters(64) + SetRoundBackdrop(editBox, T.inputBg, T.inputBorder) + editBox:SetTextInsets(8, 8, 4, 4) + editBox:SetScript("OnEscapePressed", function() + this:ClearFocus() + f:Hide() + end) + f.editBox = editBox + + local acceptBtn = MakeButton(f, "确定", 100, 24) + acceptBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOM", -4, 10) + f.acceptBtn = acceptBtn + + local cancelBtn = MakeButton(f, "取消", 100, 24) + cancelBtn:SetPoint("BOTTOMLEFT", f, "BOTTOM", 4, 10) + cancelBtn:SetScript("OnClick", function() + f:Hide() + end) + f.cancelBtn = cancelBtn + + f:Hide() + customPopup = f + end + + customPopup.titleFS:SetText(title or "") + customPopup.editBox:SetText("") + + if hasInput then + customPopup.editBox:Show() + customPopup:SetHeight(120) + else + customPopup.editBox:Hide() + customPopup:SetHeight(80) + end + + customPopup.acceptBtn:SetScript("OnClick", function() + local text = customPopup.editBox:GetText() + customPopup:Hide() + if onAccept then onAccept(text) end + end) + + customPopup.editBox:SetScript("OnEnterPressed", function() + local text = customPopup.editBox:GetText() + customPopup:Hide() + if onAccept then onAccept(text) end + end) + + customPopup:Show() + if hasInput then + customPopup.editBox:SetFocus() + end +end + +local function GetClassEN(localizedName) + if not localizedName then return nil end + return CLASS_NAME_TO_EN[localizedName] or CLASS_NAME_TO_EN[string.upper(localizedName)] +end + +local function GetClassColor(classEN) + if not classEN then return T.nameText end + local c = CLASS_COLORS[string.upper(classEN)] + if c then return c end + return T.nameText +end + +local function CreateClassIcon(parent, size) + local sz = size or 16 + local icon = parent:CreateTexture(nil, "OVERLAY") + icon:SetTexture(CLASS_ICON_PATH) + icon:SetWidth(sz) + icon:SetHeight(sz) + return icon +end + +local function SetClassIcon(icon, classEN) + if not classEN then + icon:Hide() + return + end + local upper = string.upper(classEN) + local coords = CLASS_ICON_TCOORDS[upper] + if coords then + icon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]) + icon:SetVertexColor(1, 1, 1) + icon:Show() + else + icon:Hide() + end +end + +-------------------------------------------------------------------------------- +-- Scroll Frame Helper +-------------------------------------------------------------------------------- +local function CreateScrollArea(parent, w, h) + local container = CreateFrame("Frame", nil, parent) + container:SetWidth(w) + container:SetHeight(h) + + local scrollFrame = CreateFrame("ScrollFrame", NextName("Scroll"), container) + scrollFrame:SetWidth(w) + scrollFrame:SetHeight(h) + scrollFrame:SetAllPoints(container) + + local child = CreateFrame("Frame", nil, scrollFrame) + child:SetWidth(w) + child:SetHeight(1) + scrollFrame:SetScrollChild(child) + + local offset = 0 + local maxOffset = 0 + + container.SetContentHeight = function(self, ch) + child:SetHeight(ch) + maxOffset = math.max(0, ch - h) + if offset > maxOffset then + offset = maxOffset + scrollFrame:SetVerticalScroll(offset) + end + end + + container.Reset = function(self) + offset = 0 + scrollFrame:SetVerticalScroll(0) + end + + local function DoScroll(delta) + offset = offset - delta * SCROLL_STEP + if offset < 0 then offset = 0 end + if offset > maxOffset then offset = maxOffset end + scrollFrame:SetVerticalScroll(offset) + end + + scrollFrame:EnableMouseWheel(true) + scrollFrame:SetScript("OnMouseWheel", function() + DoScroll(arg1 or 0) + end) + container:EnableMouseWheel(true) + container:SetScript("OnMouseWheel", function() + DoScroll(arg1 or 0) + end) + + container.child = child + container.scrollFrame = scrollFrame + return container +end + +-------------------------------------------------------------------------------- +-- Hide Blizzard FriendsFrame +-------------------------------------------------------------------------------- +local origFriendsFrameShow +local function HideBlizzardFriends() + if not FriendsFrame then return end + origFriendsFrameShow = FriendsFrame.Show + FriendsFrame:SetAlpha(0) + FriendsFrame:EnableMouse(false) + FriendsFrame:ClearAllPoints() + FriendsFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) + FriendsFrame.Show = function() end +end + +-------------------------------------------------------------------------------- +-- Tab 1: Friends / Ignore +-------------------------------------------------------------------------------- +local function BuildFriendsPage(page) + local subTabFrame = CreateFrame("Frame", nil, page) + subTabFrame:SetHeight(16) + subTabFrame:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + subTabFrame:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, 0) + + local friendsSubBtn = CreateFrame("Button", NextName("SubTab"), subTabFrame) + friendsSubBtn:SetWidth(50) + friendsSubBtn:SetHeight(16) + friendsSubBtn:SetPoint("LEFT", subTabFrame, "LEFT", 0, 0) + local fsBtnText = MakeFS(friendsSubBtn, 10, "CENTER", T.tabActiveText) + fsBtnText:SetPoint("CENTER", 0, 0) + fsBtnText:SetText("好友") + friendsSubBtn.text = fsBtnText + friendsSubBtn:SetScript("OnEnter", function() + if friendSubTab ~= "friends" then + this.text:SetTextColor(1, 1, 1) + end + end) + friendsSubBtn:SetScript("OnLeave", function() + if friendSubTab ~= "friends" then + this.text:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + end + end) + page.friendsSubBtn = friendsSubBtn + + local ignoreSubBtn = CreateFrame("Button", NextName("SubTab"), subTabFrame) + ignoreSubBtn:SetWidth(50) + ignoreSubBtn:SetHeight(16) + ignoreSubBtn:SetPoint("LEFT", friendsSubBtn, "RIGHT", 6, 0) + local igBtnText = MakeFS(ignoreSubBtn, 10, "CENTER", T.tabText) + igBtnText:SetPoint("CENTER", 0, 0) + igBtnText:SetText("屏蔽") + ignoreSubBtn.text = igBtnText + ignoreSubBtn:SetScript("OnEnter", function() + if friendSubTab ~= "ignore" then + this.text:SetTextColor(1, 1, 1) + end + end) + ignoreSubBtn:SetScript("OnLeave", function() + if friendSubTab ~= "ignore" then + this.text:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + end + end) + page.ignoreSubBtn = ignoreSubBtn + + -- Underline indicator for active sub-tab + local subIndicator = friendsSubBtn:CreateTexture(nil, "OVERLAY") + subIndicator:SetHeight(2) + subIndicator:SetPoint("BOTTOMLEFT", friendsSubBtn, "BOTTOMLEFT", 4, 0) + subIndicator:SetPoint("BOTTOMRIGHT", friendsSubBtn, "BOTTOMRIGHT", -4, 0) + subIndicator:SetTexture(1, 1, 1, 1) + subIndicator:SetVertexColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], 0.8) + page.subIndicator = subIndicator + + -- Friend search bar + local friendSearchBar = CreateFrame("Frame", nil, page) + friendSearchBar:SetHeight(20) + friendSearchBar:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -18) + friendSearchBar:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, -18) + page.friendSearchBar = friendSearchBar + + local searchLabel = MakeFS(friendSearchBar, 9, "LEFT", T.dimText) + searchLabel:SetPoint("LEFT", friendSearchBar, "LEFT", 2, 0) + searchLabel:SetText("搜索:") + + local friendSearchBox = MakeEditBox(friendSearchBar, CONTENT_W - 36, 18) + friendSearchBox:SetPoint("LEFT", searchLabel, "RIGHT", 4, 0) + friendSearchBox:SetScript("OnTextChanged", function() + friendSearchText = this:GetText() or "" + SUI:UpdateFriendsList() + end) + friendSearchBox:SetScript("OnEnterPressed", function() this:ClearFocus() end) + friendSearchBox:SetScript("OnEscapePressed", function() + this:SetText("") + this:ClearFocus() + end) + page.friendSearchBox = friendSearchBox + + -- Friends list + local friendsArea = CreateFrame("Frame", nil, page) + friendsArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -40) + friendsArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, BOTTOM_H) + page.friendsArea = friendsArea + + local fScroll = CreateScrollArea(friendsArea, CONTENT_W, FRAME_H - HEADER_H - TAB_BAR_H - 40 - BOTTOM_H - 16) + fScroll:SetPoint("TOPLEFT", friendsArea, "TOPLEFT", 0, 0) + page.fScroll = fScroll + + local MAX_FRIEND_ROWS = 50 + for idx = 1, MAX_FRIEND_ROWS do + local rowIdx = idx + local row = CreateFrame("Button", NextName("FR"), fScroll.child) + row:SetWidth(CONTENT_W - 4) + row:SetHeight(ROW_H) + row:SetPoint("TOPLEFT", fScroll.child, "TOPLEFT", 2, -((rowIdx - 1) * ROW_H)) + row.rowIndex = rowIdx + SetRowNormal(row) + + local classIcon = CreateClassIcon(row, 16) + classIcon:SetPoint("LEFT", row, "LEFT", 4, 0) + row.classIcon = classIcon + + local nameFS = MakeFS(row, 11, "LEFT", T.nameText) + nameFS:SetPoint("LEFT", classIcon, "RIGHT", 4, 0) + nameFS:SetWidth(120) + row.nameFS = nameFS + + local infoFS = MakeFS(row, 9, "LEFT", T.dimText) + infoFS:SetPoint("LEFT", nameFS, "RIGHT", 4, 0) + infoFS:SetPoint("RIGHT", row, "RIGHT", -4, 0) + row.infoFS = infoFS + + AddSelHighlight(row) + row:RegisterForClicks("LeftButtonUp", "RightButtonUp") + + row:SetScript("OnEnter", function() + SetPixelBackdrop(this, T.slotHover, T.slotSelected) + end) + row:SetScript("OnLeave", function() + if selectedFriend and selectedFriend == this.friendDataIndex then + SetPixelBackdrop(this, T.tabActiveBg, T.tabActiveBorder) + else + SetRowNormal(this) + end + end) + row:SetScript("OnClick", function() + selectedFriend = this.friendDataIndex + if arg1 == "RightButton" and this.friendDataIndex then + SUI:ShowFriendRowMenu(this) + return + end + SUI:UpdateFriendsList() + end) + row:Hide() + friendRows[idx] = row + end + + -- Ignore list + local ignoreArea = CreateFrame("Frame", nil, page) + ignoreArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -20) + ignoreArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, BOTTOM_H) + ignoreArea:Hide() + page.ignoreArea = ignoreArea + + local iScroll = CreateScrollArea(ignoreArea, CONTENT_W, FRAME_H - HEADER_H - TAB_BAR_H - 20 - BOTTOM_H - 16) + iScroll:SetPoint("TOPLEFT", ignoreArea, "TOPLEFT", 0, 0) + page.iScroll = iScroll + + local MAX_IGNORE_ROWS = 50 + for idx = 1, MAX_IGNORE_ROWS do + local rowIdx = idx + local row = CreateFrame("Button", NextName("IR"), iScroll.child) + row:SetWidth(CONTENT_W - 4) + row:SetHeight(20) + row:SetPoint("TOPLEFT", iScroll.child, "TOPLEFT", 2, -((rowIdx - 1) * 20)) + row.rowIndex = rowIdx + SetRowNormal(row) + + local nameFS = MakeFS(row, 11, "LEFT", T.nameText) + nameFS:SetPoint("LEFT", row, "LEFT", 8, 0) + row.nameFS = nameFS + + row:SetScript("OnEnter", function() + SetPixelBackdrop(this, T.slotHover, T.slotSelected) + end) + row:SetScript("OnLeave", function() + SetRowNormal(this) + end) + row:Hide() + ignoreRows[idx] = row + end + + -- Bottom buttons + local btnBar = CreateFrame("Frame", nil, page) + btnBar:SetHeight(BOTTOM_H) + btnBar:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0) + btnBar:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) + page.btnBar = btnBar + + local addBtn = MakeButton(btnBar, "添加好友", 80, 22) + addBtn:SetPoint("BOTTOMLEFT", btnBar, "BOTTOMLEFT", 0, 2) + addBtn:SetScript("OnClick", function() + if friendSubTab == "friends" then + ShowCustomPopup("添加好友", true, function(name) + if name and name ~= "" then AddFriend(name) end + end) + else + ShowCustomPopup("添加屏蔽", true, function(name) + if name and name ~= "" then AddIgnore(name) end + end) + end + end) + + local removeBtn = MakeButton(btnBar, "删除好友", 80, 22) + removeBtn:SetPoint("LEFT", addBtn, "RIGHT", 4, 0) + removeBtn:SetScript("OnClick", function() + if selectedFriend then RemoveFriend(selectedFriend) end + end) + + local msgBtn = MakeButton(btnBar, "发送信息", 80, 22) + msgBtn:SetPoint("LEFT", removeBtn, "RIGHT", 4, 0) + msgBtn:SetScript("OnClick", function() + if selectedFriend then + local name = GetFriendInfo(selectedFriend) + if name then ChatFrame_SendTell(name) end + end + end) + + local inviteBtn = MakeButton(btnBar, "组队邀请", 80, 22) + inviteBtn:SetPoint("LEFT", msgBtn, "RIGHT", 4, 0) + inviteBtn:SetScript("OnClick", function() + if selectedFriend then + local name = GetFriendInfo(selectedFriend) + if name then InviteByName(name) end + end + end) + + page.addBtn = addBtn + page.removeBtn = removeBtn + page.msgBtn = msgBtn + page.inviteBtn = inviteBtn + + friendsSubBtn:SetScript("OnClick", function() + friendSubTab = "friends" + SUI:UpdateFriendsPage() + end) + ignoreSubBtn:SetScript("OnClick", function() + friendSubTab = "ignore" + SUI:UpdateFriendsPage() + end) +end + +function SUI:UpdateFriendsPage() + local page = pages[1] + if not page then return end + + if friendSubTab == "friends" then + page.friendsArea:Show() + page.friendSearchBar:Show() + page.ignoreArea:Hide() + page.friendsSubBtn.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + page.ignoreSubBtn.text:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + page.subIndicator:ClearAllPoints() + page.subIndicator:SetPoint("BOTTOMLEFT", page.friendsSubBtn, "BOTTOMLEFT", 4, 0) + page.subIndicator:SetPoint("BOTTOMRIGHT", page.friendsSubBtn, "BOTTOMRIGHT", -4, 0) + page.subIndicator:Show() + page.addBtn.text:SetText("添加好友") + page.removeBtn.text:SetText("删除好友") + self:UpdateFriendsList() + else + page.friendsArea:Hide() + page.friendSearchBar:Hide() + page.ignoreArea:Show() + page.ignoreSubBtn.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + page.friendsSubBtn.text:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + page.subIndicator:ClearAllPoints() + page.subIndicator:SetPoint("BOTTOMLEFT", page.ignoreSubBtn, "BOTTOMLEFT", 4, 0) + page.subIndicator:SetPoint("BOTTOMRIGHT", page.ignoreSubBtn, "BOTTOMRIGHT", -4, 0) + page.subIndicator:Show() + page.addBtn.text:SetText("添加屏蔽") + page.removeBtn.text:SetText("取消屏蔽") + self:UpdateIgnoreList() + end +end + +function SUI:UpdateFriendsList() + local numFriends = GetNumFriends() + local totalH = 0 + local rowIdx = 0 + local searchLower = string.lower(friendSearchText or "") + local hasSearch = searchLower ~= "" + + for i = 1, numFriends do + local name, level, class, area, connected, status = GetFriendInfo(i) + if name then + local match = true + if hasSearch then + match = string.find(string.lower(name), searchLower, 1, true) + if not match and class then + match = string.find(string.lower(class), searchLower, 1, true) + end + if not match and area then + match = string.find(string.lower(area), searchLower, 1, true) + end + end + + if match then + rowIdx = rowIdx + 1 + if rowIdx > 50 then break end + local row = friendRows[rowIdx] + if row then + row.nameFS:SetText(name) + row.friendDataIndex = i + local classEN = GetClassEN(class) + + if connected then + local cc = classEN and GetClassColor(classEN) or T.onlineText + row.nameFS:SetTextColor(cc[1], cc[2], cc[3]) + local info = "" + if level and level > 0 then info = "等级" .. level end + if class then info = info .. " " .. class end + if area then info = info .. " - " .. area end + row.infoFS:SetText(info) + row.infoFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + else + row.nameFS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) + row.infoFS:SetText("离线") + row.infoFS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) + end + + SetClassIcon(row.classIcon, classEN) + + if selectedFriend == i then + SetPixelBackdrop(row, T.tabActiveBg, T.tabActiveBorder) + ShowSelHighlight(row) + row.nameFS:SetTextColor(1, 1, 1) + else + SetRowNormal(row) + HideSelHighlight(row) + end + + row:Show() + totalH = totalH + ROW_H + end + end + end + end + + for i = rowIdx + 1, 50 do + if friendRows[i] then friendRows[i]:Hide() end + end + + if pages[1] and pages[1].fScroll then + pages[1].fScroll:SetContentHeight(totalH + 4) + end +end + +function SUI:UpdateIgnoreList() + local numIgnores = GetNumIgnores() + local totalH = 0 + for i = 1, 50 do + local row = ignoreRows[i] + if not row then break end + if i <= numIgnores then + local name = GetIgnoreName(i) + if name then + row.nameFS:SetText(name) + row:Show() + totalH = totalH + 20 + else + row:Hide() + end + else + row:Hide() + end + end + if pages[1] and pages[1].iScroll then + pages[1].iScroll:SetContentHeight(totalH + 4) + end +end + +function SUI:ShowFriendRowMenu(row) + local di = row.friendDataIndex + if not di then return end + local name, level, class, area, connected, status = GetFriendInfo(di) + if not name then return end + + if not SUI.friendDropDown then + SUI.friendDropDown = CreateFrame("Frame", "SFramesSocialFriendDD", UIParent, "UIDropDownMenuTemplate") + SUI.friendDropDown.displayMode = "MENU" + SUI.friendDropDown.initialize = function() + local fd = SUI.friendDropDown + local fName = fd.friendName + local fOnline = fd.friendOnline + if not fName then return end + + local info = {} + info.text = fName + info.isTitle = 1 + info.notCheckable = 1 + UIDropDownMenu_AddButton(info) + + if fOnline then + info = {} + info.text = "密语" + info.notCheckable = 1 + info.func = function() ChatFrame_SendTell(fName) end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = "邀请组队" + info.notCheckable = 1 + info.func = function() InviteByName(fName) end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = "观察" + info.notCheckable = 1 + info.func = function() + if TargetByName then TargetByName(fName) end + if UnitExists("target") and UnitName("target") == fName then + InspectUnit("target") + end + end + UIDropDownMenu_AddButton(info) + end + + info = {} + info.text = "删除好友" + info.notCheckable = 1 + info.func = function() + local idx = fd.friendIdx + if idx then RemoveFriend(idx) end + end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = "取消" + info.notCheckable = 1 + UIDropDownMenu_AddButton(info) + end + end + + SUI.friendDropDown.friendName = name + SUI.friendDropDown.friendOnline = connected + SUI.friendDropDown.friendIdx = di + ToggleDropDownMenu(1, nil, SUI.friendDropDown, "cursor") +end + +-------------------------------------------------------------------------------- +-- Tab 2: Who +-------------------------------------------------------------------------------- +local function BuildWhoPage(page) + local searchBar = CreateFrame("Frame", nil, page) + searchBar:SetHeight(26) + searchBar:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + searchBar:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, 0) + + local editBox = MakeEditBox(searchBar, CONTENT_W - 70, 22) + editBox:SetPoint("LEFT", searchBar, "LEFT", 0, 0) + editBox:SetScript("OnEnterPressed", function() + local text = this:GetText() + if text and text ~= "" then SendWho(text) end + this:ClearFocus() + end) + page.editBox = editBox + + local searchBtn = MakeButton(searchBar, "搜索", 64, 22) + searchBtn:SetPoint("LEFT", editBox, "RIGHT", 4, 0) + searchBtn:SetScript("OnClick", function() + local text = page.editBox:GetText() + if text and text ~= "" then SendWho(text) end + end) + + -- Column headers + local colFrame = CreateFrame("Frame", nil, page) + colFrame:SetHeight(18) + colFrame:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -28) + colFrame:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, -28) + + local cols = { { "名称", 0.30 }, { "等级", 0.12 }, { "职业", 0.18 }, { "区域", 0.40 } } + local cx = 0 + for _, col in ipairs(cols) do + local fs = MakeFS(colFrame, 10, "LEFT", T.colHeader) + fs:SetPoint("TOPLEFT", colFrame, "TOPLEFT", cx, 0) + fs:SetWidth(CONTENT_W * col[2]) + fs:SetText(col[1]) + cx = cx + CONTENT_W * col[2] + end + MakeSep(page, -46) + + -- Results scroll + local listArea = CreateFrame("Frame", nil, page) + listArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -48) + listArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, BOTTOM_H) + + local wScroll = CreateScrollArea(listArea, CONTENT_W, FRAME_H - HEADER_H - TAB_BAR_H - 48 - BOTTOM_H - 16) + wScroll:SetPoint("TOPLEFT", listArea, "TOPLEFT", 0, 0) + page.wScroll = wScroll + + local MAX_WHO_ROWS = 50 + for idx = 1, MAX_WHO_ROWS do + local rowIdx = idx + local row = CreateFrame("Button", NextName("WR"), wScroll.child) + row:SetWidth(CONTENT_W - 4) + row:SetHeight(20) + row:SetPoint("TOPLEFT", wScroll.child, "TOPLEFT", 2, -((rowIdx - 1) * 20)) + row.rowIndex = rowIdx + SetRowNormal(row) + + local nameFS = MakeFS(row, 10, "LEFT", T.nameText) + nameFS:SetPoint("LEFT", row, "LEFT", 4, 0) + nameFS:SetWidth(CONTENT_W * 0.30 - 8) + row.nameFS = nameFS + + local lvlFS = MakeFS(row, 10, "LEFT", T.bodyText) + lvlFS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.30, 0) + lvlFS:SetWidth(CONTENT_W * 0.12) + row.lvlFS = lvlFS + + local classFS = MakeFS(row, 10, "LEFT", T.bodyText) + classFS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.42, 0) + classFS:SetWidth(CONTENT_W * 0.18) + row.classFS = classFS + + local zoneFS = MakeFS(row, 10, "LEFT", T.dimText) + zoneFS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.60, 0) + zoneFS:SetPoint("RIGHT", row, "RIGHT", -4, 0) + row.zoneFS = zoneFS + + AddSelHighlight(row) + row:SetScript("OnEnter", function() + SetPixelBackdrop(this, T.slotHover, T.slotSelected) + end) + row:SetScript("OnLeave", function() + if selectedWho == this.rowIndex then + SetPixelBackdrop(this, T.tabActiveBg, T.tabActiveBorder) + else + SetRowNormal(this) + end + end) + row:SetScript("OnClick", function() + selectedWho = this.rowIndex + SUI:UpdateWhoList() + end) + row:Hide() + whoRows[idx] = row + end + + -- Totals + local totalFS = MakeFS(page, 10, "LEFT", T.dimText) + totalFS:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, BOTTOM_H + 2) + page.totalFS = totalFS + + -- Bottom buttons + local btnBar = CreateFrame("Frame", nil, page) + btnBar:SetHeight(BOTTOM_H) + btnBar:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0) + btnBar:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) + + local invBtn = MakeButton(btnBar, "组队邀请", 80, 22) + invBtn:SetPoint("BOTTOMLEFT", btnBar, "BOTTOMLEFT", 0, 2) + invBtn:SetScript("OnClick", function() + if selectedWho then + local name = GetWhoInfo(selectedWho) + if name then InviteByName(name) end + end + end) + + local addFriendBtn = MakeButton(btnBar, "添加好友", 80, 22) + addFriendBtn:SetPoint("LEFT", invBtn, "RIGHT", 4, 0) + addFriendBtn:SetScript("OnClick", function() + if selectedWho then + local name = GetWhoInfo(selectedWho) + if name then AddFriend(name) end + end + end) + + local whisperBtn = MakeButton(btnBar, "密语", 80, 22) + whisperBtn:SetPoint("LEFT", addFriendBtn, "RIGHT", 4, 0) + whisperBtn:SetScript("OnClick", function() + if selectedWho then + local name = GetWhoInfo(selectedWho) + if name then ChatFrame_SendTell(name) end + end + end) +end + +function SUI:UpdateWhoList() + local numWho, totalCount = GetNumWhoResults() + local totalH = 0 + for i = 1, 50 do + local row = whoRows[i] + if not row then break end + if i <= numWho then + local name, guild, level, race, class, zone, classEN = GetWhoInfo(i) + if name then + if not classEN or classEN == "" then + classEN = GetClassEN(class) + end + local cc = GetClassColor(classEN) + row.nameFS:SetText(name) + row.nameFS:SetTextColor(cc[1], cc[2], cc[3]) + row.lvlFS:SetText(level or "") + row.classFS:SetText(class or "") + row.classFS:SetTextColor(cc[1], cc[2], cc[3]) + row.zoneFS:SetText(zone or "") + + if selectedWho == i then + SetPixelBackdrop(row, T.tabActiveBg, T.tabActiveBorder) + ShowSelHighlight(row) + row.nameFS:SetTextColor(1, 1, 1) + else + SetRowNormal(row) + HideSelHighlight(row) + end + row:Show() + totalH = totalH + 20 + else + row:Hide() + end + else + row:Hide() + end + end + if pages[2] and pages[2].wScroll then + pages[2].wScroll:SetContentHeight(totalH + 4) + end + if pages[2] and pages[2].totalFS then + pages[2].totalFS:SetText((numWho or 0) .. " 个结果 (共 " .. (totalCount or 0) .. " 人)") + end +end + +-------------------------------------------------------------------------------- +-- Tab 3: Guild +-------------------------------------------------------------------------------- +local function BuildGuildPage(page) + local motdFrame = CreateFrame("Frame", nil, page) + motdFrame:SetHeight(36) + motdFrame:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + motdFrame:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, 0) + SetPixelBackdrop(motdFrame, { 0.08, 0.04, 0.06, 0.8 }, T.slotBorder) + + local motdLabel = MakeFS(motdFrame, 9, "LEFT", T.dimText) + motdLabel:SetPoint("TOPLEFT", motdFrame, "TOPLEFT", 6, -2) + motdLabel:SetText("公会公告:") + + local motdText = MakeFS(motdFrame, 10, "LEFT", T.bodyText) + motdText:SetPoint("TOPLEFT", motdLabel, "BOTTOMLEFT", 0, -2) + motdText:SetPoint("RIGHT", motdFrame, "RIGHT", -6, 0) + page.motdText = motdText + + -- Toolbar row: search + filter buttons + local toolBar = CreateFrame("Frame", nil, page) + toolBar:SetHeight(18) + toolBar:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -38) + toolBar:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, -38) + + local gSearchLabel = MakeFS(toolBar, 9, "LEFT", T.dimText) + gSearchLabel:SetPoint("LEFT", toolBar, "LEFT", 0, 0) + gSearchLabel:SetText("搜索:") + + local gSearchBox = MakeEditBox(toolBar, 110, 16) + gSearchBox:SetPoint("LEFT", gSearchLabel, "RIGHT", 2, 0) + gSearchBox:SetFont(GetFont(), 9, "OUTLINE") + gSearchBox:SetScript("OnTextChanged", function() + guildSearchText = this:GetText() or "" + SUI:UpdateGuildList() + end) + gSearchBox:SetScript("OnEnterPressed", function() this:ClearFocus() end) + gSearchBox:SetScript("OnEscapePressed", function() + this:SetText("") + this:ClearFocus() + end) + page.gSearchBox = gSearchBox + + local offlineToggle = MakeButton(toolBar, "隐藏离线", 56, 16) + offlineToggle:SetPoint("TOPRIGHT", toolBar, "TOPRIGHT", 0, 0) + offlineToggle.text:SetFont(GetFont(), 9, "OUTLINE") + offlineToggle:SetScript("OnClick", function() + guildHideOffline = not guildHideOffline + if guildHideOffline then + this.text:SetText("显示离线") + else + this.text:SetText("隐藏离线") + end + SUI:UpdateGuildList() + end) + page.offlineToggle = offlineToggle + + local viewToggle = MakeButton(toolBar, "切换视图", 56, 16) + viewToggle:SetPoint("RIGHT", offlineToggle, "LEFT", -4, 0) + viewToggle.text:SetFont(GetFont(), 9, "OUTLINE") + viewToggle:SetScript("OnClick", function() + if guildViewMode == "player" then guildViewMode = "guild" else guildViewMode = "player" end + SUI:UpdateGuildList() + end) + + -- Column headers + local colFrame = CreateFrame("Frame", nil, page) + colFrame:SetHeight(18) + colFrame:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -56) + colFrame:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, -56) + page.guildColFrame = colFrame + page.guildColBtns = {} + + MakeSep(page, -74) + + local listArea = CreateFrame("Frame", nil, page) + listArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -76) + listArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, BOTTOM_H + 14) + + local gScroll = CreateScrollArea(listArea, CONTENT_W, FRAME_H - HEADER_H - TAB_BAR_H - 76 - BOTTOM_H - 14 - 16) + gScroll:SetPoint("TOPLEFT", listArea, "TOPLEFT", 0, 0) + page.gScroll = gScroll + + local MAX_GUILD_ROWS = 100 + for idx = 1, MAX_GUILD_ROWS do + local rowIdx = idx + local row = CreateFrame("Button", NextName("GR"), gScroll.child) + row:SetWidth(CONTENT_W - 4) + row:SetHeight(18) + row:SetPoint("TOPLEFT", gScroll.child, "TOPLEFT", 2, -((rowIdx - 1) * 18)) + row.rowIndex = rowIdx + SetRowNormal(row) + + local classIcon = CreateClassIcon(row, 14) + classIcon:SetPoint("LEFT", row, "LEFT", 2, 0) + row.classIcon = classIcon + + local nameFS = MakeFS(row, 10, "LEFT", T.nameText) + nameFS:SetPoint("LEFT", classIcon, "RIGHT", 3, 0) + nameFS:SetWidth(CONTENT_W * 0.24 - 20) + row.nameFS = nameFS + + local lvlFS = MakeFS(row, 10, "CENTER", T.bodyText) + lvlFS:SetWidth(30) + row.lvlFS = lvlFS + + local col3FS = MakeFS(row, 10, "LEFT", T.dimText) + col3FS:SetWidth(100) + row.col3FS = col3FS + + local col4FS = MakeFS(row, 9, "LEFT", T.dimText) + row.col4FS = col4FS + + local col5FS = MakeFS(row, 9, "LEFT", T.dimText) + row.col5FS = col5FS + + AddSelHighlight(row) + row:RegisterForClicks("LeftButtonUp", "RightButtonUp") + + row:SetScript("OnEnter", function() + SetPixelBackdrop(this, T.slotHover, T.slotSelected) + end) + row:SetScript("OnLeave", function() + if selectedGuild and selectedGuild == this.guildDataIndex then + SetPixelBackdrop(this, T.tabActiveBg, T.tabActiveBorder) + else + SetRowNormal(this) + end + end) + row:SetScript("OnClick", function() + selectedGuild = this.guildDataIndex + if arg1 == "RightButton" and this.guildDataIndex then + SUI:ShowGuildRowMenu(this) + else + SUI:UpdateGuildList() + end + end) + row:Hide() + guildRows[idx] = row + end + + local totalFS = MakeFS(page, 9, "LEFT", T.dimText) + totalFS:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 2, BOTTOM_H + 2) + page.guildTotalFS = totalFS + + local btnBar = CreateFrame("Frame", nil, page) + btnBar:SetHeight(BOTTOM_H) + btnBar:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0) + btnBar:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) + + local invBtn = MakeButton(btnBar, "组队邀请", 80, 22) + invBtn:SetPoint("BOTTOMLEFT", btnBar, "BOTTOMLEFT", 0, 2) + invBtn:SetScript("OnClick", function() + if selectedGuild then + local name, rank, rankIndex, level, class, zone, note, officerNote, online = GetGuildRosterInfo(selectedGuild) + if name and online then InviteByName(name) end + end + end) + + local msgBtn = MakeButton(btnBar, "发送信息", 80, 22) + msgBtn:SetPoint("LEFT", invBtn, "RIGHT", 4, 0) + msgBtn:SetScript("OnClick", function() + if selectedGuild then + local name = GetGuildRosterInfo(selectedGuild) + if name then ChatFrame_SendTell(name) end + end + end) + + local gInviteBtn = MakeButton(btnBar, "邀请入会", 80, 22) + gInviteBtn:SetPoint("LEFT", msgBtn, "RIGHT", 4, 0) + gInviteBtn:SetScript("OnClick", function() + ShowCustomPopup("邀请玩家加入公会", true, function(name) + if name and name ~= "" then + if GuildInvite then + GuildInvite(name) + elseif GuildInviteByName then + GuildInviteByName(name) + else + SendChatMessage(".ginvite " .. name, "GUILD") + end + end + end) + end) + page.gInviteBtn = gInviteBtn + + local gLeaveBtn = MakeButton(btnBar, "退出公会", 80, 22) + gLeaveBtn:SetPoint("LEFT", gInviteBtn, "RIGHT", 4, 0) + gLeaveBtn:SetScript("OnClick", function() + StaticPopup_Show("CONFIRM_GUILD_LEAVE", GetGuildInfo("player")) + end) + page.gLeaveBtn = gLeaveBtn +end + +function SUI:ShowGuildRowMenu(row) + local di = row.guildDataIndex + if not di then return end + local name, rank, rankIndex, level, class, zone, note, officerNote, online = GetGuildRosterInfo(di) + if not name then return end + local isSelf = (name == UnitName("player")) + + if not SUI.guildDropDown then + SUI.guildDropDown = CreateFrame("Frame", "SFramesSocialGuildDD", UIParent, "UIDropDownMenuTemplate") + SUI.guildDropDown.displayMode = "MENU" + SUI.guildDropDown.initialize = function() + local gd = SUI.guildDropDown + local gName = gd.memberName + local gOnline = gd.memberOnline + local gSelf = gd.memberIsSelf + if not gName then return end + + local info = {} + info.text = gName + info.isTitle = 1 + info.notCheckable = 1 + UIDropDownMenu_AddButton(info) + + if not gSelf then + if gOnline then + info = {} + info.text = "密语" + info.notCheckable = 1 + info.func = function() ChatFrame_SendTell(gName) end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = "邀请组队" + info.notCheckable = 1 + info.func = function() InviteByName(gName) end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = "观察" + info.notCheckable = 1 + info.func = function() + if TargetByName then TargetByName(gName) end + if UnitExists("target") and UnitName("target") == gName then + InspectUnit("target") + end + end + UIDropDownMenu_AddButton(info) + end + + if CanGuildRemove and CanGuildRemove() then + info = {} + info.text = "移出公会" + info.notCheckable = 1 + info.func = function() GuildUninvite(gName) end + UIDropDownMenu_AddButton(info) + end + + if CanGuildPromote and CanGuildPromote() then + info = {} + info.text = "晋升" + info.notCheckable = 1 + info.func = function() GuildPromote(gName) end + UIDropDownMenu_AddButton(info) + end + + if CanGuildDemote and CanGuildDemote() then + info = {} + info.text = "降级" + info.notCheckable = 1 + info.func = function() GuildDemote(gName) end + UIDropDownMenu_AddButton(info) + end + else + info = {} + info.text = "退出公会" + info.notCheckable = 1 + info.func = function() + StaticPopup_Show("CONFIRM_GUILD_LEAVE", GetGuildInfo("player")) + end + UIDropDownMenu_AddButton(info) + end + + info = {} + info.text = "取消" + info.notCheckable = 1 + UIDropDownMenu_AddButton(info) + end + end + + SUI.guildDropDown.memberName = name + SUI.guildDropDown.memberOnline = online + SUI.guildDropDown.memberIsSelf = isSelf + ToggleDropDownMenu(1, nil, SUI.guildDropDown, "cursor") +end + +local GUILD_COL_SORT_MAP = { + ["名称"] = "name", + ["等级"] = "level", + ["职业"] = "class", + ["职位"] = "rank", + ["区域"] = "zone", + ["备注"] = "note", + ["状态"] = "status", +} + +local function RebuildGuildColHeaders() + local colFrame = pages[3] and pages[3].guildColFrame + if not colFrame then return end + local old = pages[3].guildColBtns + if old then + for _, btn in ipairs(old) do btn:Hide() end + end + pages[3].guildColBtns = {} + local headers + if guildViewMode == "player" then + headers = { { "名称", 0, 0.24 }, { "等级", 0.24, 0.08 }, { "职业", 0.32, 0.14 }, { "职位", 0.46, 0.18 }, { "区域", 0.64, 0.36 } } + else + headers = { { "名称", 0, 0.30 }, { "职位", 0.30, 0.20 }, { "备注", 0.50, 0.30 }, { "状态", 0.80, 0.20 } } + end + for _, h in ipairs(headers) do + local sortKey = GUILD_COL_SORT_MAP[h[1]] + local btn = CreateFrame("Button", nil, colFrame) + btn:SetHeight(18) + btn:SetPoint("TOPLEFT", colFrame, "TOPLEFT", CONTENT_W * h[2], 0) + btn:SetWidth(CONTENT_W * h[3]) + local fs = MakeFS(btn, 10, "LEFT", T.colHeader) + fs:SetPoint("LEFT", btn, "LEFT", 0, 0) + local arrow = "" + if sortKey and guildSortField == sortKey then + arrow = guildSortAsc and " ▲" or " ▼" + end + fs:SetText(h[1] .. arrow) + btn.headerFS = fs + if sortKey then + btn.sortKey = sortKey + btn:SetScript("OnClick", function() + if guildSortField == this.sortKey then + guildSortAsc = not guildSortAsc + else + guildSortField = this.sortKey + guildSortAsc = true + end + SUI:UpdateGuildList() + end) + btn:SetScript("OnEnter", function() + this.headerFS:SetTextColor(1, 1, 1) + end) + btn:SetScript("OnLeave", function() + this.headerFS:SetTextColor(T.colHeader[1], T.colHeader[2], T.colHeader[3]) + end) + end + table.insert(pages[3].guildColBtns, btn) + end +end + +function SUI:UpdateGuildList() + if not IsInGuild() then + for i = 1, 100 do + if guildRows[i] then guildRows[i]:Hide() end + end + if pages[3] and pages[3].motdText then + pages[3].motdText:SetText("你没有加入公会") + end + if pages[3] and pages[3].gInviteBtn then pages[3].gInviteBtn:Hide() end + if pages[3] and pages[3].gLeaveBtn then pages[3].gLeaveBtn:Hide() end + if pages[3] and pages[3].guildTotalFS then pages[3].guildTotalFS:SetText("") end + return + end + + if SetGuildRosterShowOffline then + SetGuildRosterShowOffline(not guildHideOffline) + end + GuildRoster() + local motd = GetGuildRosterMOTD and GetGuildRosterMOTD() or "" + if pages[3] and pages[3].motdText then + pages[3].motdText:SetText(motd ~= "" and motd or "无公告") + end + + RebuildGuildColHeaders() + + local numTotal, numOnlineRet = GetNumGuildMembers() + numTotal = numTotal or 0 + local totalOnline = 0 + local members = {} + + local searchLower = string.lower(guildSearchText or "") + local hasSearch = searchLower ~= "" + + for i = 1, numTotal do + local name, rank, rankIndex, level, class, zone, note, officerNote, online, status, classEN = GetGuildRosterInfo(i) + if name then + if online then totalOnline = totalOnline + 1 end + local match = true + if hasSearch then + match = string.find(string.lower(name), searchLower, 1, true) + if not match and class then + match = string.find(string.lower(class), searchLower, 1, true) + end + if not match and rank then + match = string.find(string.lower(rank), searchLower, 1, true) + end + if not match and zone then + match = string.find(string.lower(zone), searchLower, 1, true) + end + end + if match then + if not classEN or classEN == "" then + classEN = GetClassEN(class) + end + table.insert(members, { + idx = i, name = name, rank = rank, rankIndex = rankIndex or 99, + level = level or 0, class = class or "", classEN = classEN, + zone = zone or "", note = note or "", officerNote = officerNote or "", + online = online, status = status, + }) + end + end + end + + local sf = guildSortField + local asc = guildSortAsc + table.sort(members, function(a, b) + local va, vb + if sf == "name" then + va, vb = (a.name or ""), (b.name or "") + elseif sf == "level" then + va, vb = (a.level or 0), (b.level or 0) + elseif sf == "class" then + va, vb = (a.class or ""), (b.class or "") + elseif sf == "rank" then + va, vb = (a.rankIndex or 99), (b.rankIndex or 99) + elseif sf == "zone" then + va, vb = (a.zone or ""), (b.zone or "") + elseif sf == "note" then + va, vb = (a.note or ""), (b.note or "") + elseif sf == "status" then + local sa = a.online and 0 or 1 + local sb = b.online and 0 or 1 + va, vb = sa, sb + else + va, vb = (a.name or ""), (b.name or "") + end + if va == vb then + return (a.name or "") < (b.name or "") + end + if asc then return va < vb else return va > vb end + end) + + local totalH = 0 + local rowIdx = 0 + + for mi = 1, table.getn(members) do + local m = members[mi] + rowIdx = rowIdx + 1 + if rowIdx > 100 then break end + local row = guildRows[rowIdx] + if row then + local cc = GetClassColor(m.classEN) + SetClassIcon(row.classIcon, m.classEN) + row.nameFS:SetText(m.name) + row.guildDataIndex = m.idx + + if m.online then + row.nameFS:SetTextColor(cc[1], cc[2], cc[3]) + else + row.nameFS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) + end + + if guildViewMode == "player" then + row.lvlFS:SetText(m.level > 0 and m.level or "") + row.lvlFS:ClearAllPoints() + row.lvlFS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.24, 0) + + row.col3FS:SetText(m.class) + row.col3FS:ClearAllPoints() + row.col3FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.32, 0) + row.col3FS:SetWidth(CONTENT_W * 0.14) + row.col3FS:SetTextColor(cc[1], cc[2], cc[3]) + + row.col4FS:SetText(m.rank or "") + row.col4FS:ClearAllPoints() + row.col4FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.46, 0) + row.col4FS:SetWidth(CONTENT_W * 0.18) + row.col4FS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + + row.col5FS:SetText(m.online and m.zone or "离线") + row.col5FS:ClearAllPoints() + row.col5FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.64, 0) + row.col5FS:SetPoint("RIGHT", row, "RIGHT", -4, 0) + if m.online then + row.col5FS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + else + row.col5FS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) + end + row.col5FS:Show() + else + row.lvlFS:SetText("") + row.col3FS:SetText(m.rank or "") + row.col3FS:ClearAllPoints() + row.col3FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.30, 0) + row.col3FS:SetWidth(CONTENT_W * 0.20) + row.col3FS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + + row.col4FS:SetText(m.note) + row.col4FS:ClearAllPoints() + row.col4FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.50, 0) + row.col4FS:SetWidth(CONTENT_W * 0.30) + row.col4FS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + + row.col5FS:SetText(m.online and "在线" or "离线") + row.col5FS:ClearAllPoints() + row.col5FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.80, 0) + row.col5FS:SetPoint("RIGHT", row, "RIGHT", -4, 0) + if m.online then + row.col5FS:SetTextColor(T.onlineText[1], T.onlineText[2], T.onlineText[3]) + else + row.col5FS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) + end + row.col5FS:Show() + end + + if selectedGuild == m.idx then + SetPixelBackdrop(row, T.tabActiveBg, T.tabActiveBorder) + ShowSelHighlight(row) + row.nameFS:SetTextColor(1, 1, 1) + else + SetRowNormal(row) + HideSelHighlight(row) + end + row:Show() + totalH = totalH + 18 + end + end + + for i = rowIdx + 1, 100 do + if guildRows[i] then + guildRows[i]:Hide() + end + end + + if pages[3] and pages[3].gScroll then + pages[3].gScroll:SetContentHeight(totalH + 4) + end + if pages[3] and pages[3].guildTotalFS then + local shownCount = table.getn(members) + if guildHideOffline then + pages[3].guildTotalFS:SetText(totalOnline .. " 人在线 (共 " .. numTotal .. " 名成员)") + else + pages[3].guildTotalFS:SetText("共 " .. numTotal .. " 名成员, " .. totalOnline .. " 人在线") + end + end + + -- Show/hide guild invite button based on permission + if pages[3] and pages[3].gInviteBtn then + local canInvite = CanGuildInvite and CanGuildInvite() + if canInvite then + pages[3].gInviteBtn:Show() + else + pages[3].gInviteBtn:Hide() + end + end +end + +-------------------------------------------------------------------------------- +-- Tab 4: Raid +-------------------------------------------------------------------------------- +local RAID_COLS = 5 +local RAID_GROUPS = 8 +local RAID_SLOT_W = 68 +local RAID_SLOT_H = 24 +local RAID_GROUP_PAD = 3 +local RAID_GROUP_LABEL_H = 14 +local RAID_ICON_SIZE = 14 + +local dragSlot = nil +local dragHighlight = nil + +local function BuildRaidPage(page) + local infoFS = MakeFS(page, 10, "LEFT", T.dimText) + infoFS:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) + page.raidInfoFS = infoFS + + local rScroll = CreateScrollArea(page, CONTENT_W, FRAME_H - HEADER_H - TAB_BAR_H - 20 - BOTTOM_H - 16) + rScroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -18) + page.rScroll = rScroll + + local groupH = RAID_GROUP_LABEL_H + RAID_SLOT_H + for gIdx = 1, RAID_GROUPS do + local g = gIdx + local groupFrame = CreateFrame("Frame", nil, rScroll.child) + groupFrame:SetWidth(CONTENT_W) + groupFrame:SetHeight(groupH) + local gy = -((g - 1) * (groupH + RAID_GROUP_PAD)) + groupFrame:SetPoint("TOPLEFT", rScroll.child, "TOPLEFT", 0, gy) + SetRoundBackdrop(groupFrame, T.raidGroup, T.raidGroupBorder) + + groupFrame.groupIndex = g + groupFrame:EnableMouse(true) + groupFrame:SetScript("OnMouseUp", function() + if not dragSlot then return end + if dragHighlight then + dragHighlight:Hide() + dragHighlight:SetScript("OnUpdate", nil) + end + local tg = this.groupIndex + if tg and dragSlot.raidIndex and tg ~= dragSlot.group then + SetRaidSubgroup(dragSlot.raidIndex, tg) + end + dragSlot = nil + end) + + local groupLabel = MakeFS(groupFrame, 9, "LEFT", T.gold) + groupLabel:SetPoint("TOPLEFT", groupFrame, "TOPLEFT", 4, -1) + groupLabel:SetText("小组 " .. g) + + for sIdx = 1, RAID_COLS do + local s = sIdx + local slot = CreateFrame("Button", NextName("RS"), groupFrame) + slot:SetWidth(RAID_SLOT_W) + slot:SetHeight(RAID_SLOT_H) + local sx = 2 + (s - 1) * (RAID_SLOT_W + 2) + slot:SetPoint("BOTTOMLEFT", groupFrame, "BOTTOMLEFT", sx, 1) + SetPixelBackdrop(slot, T.raidSlotEmpty, T.slotBorder) + slot.group = g + slot.slotIndex = s + + local classIcon = CreateClassIcon(slot, RAID_ICON_SIZE) + classIcon:SetPoint("LEFT", slot, "LEFT", 2, 0) + slot.classIcon = classIcon + + local nameFS = MakeFS(slot, 8, "LEFT", T.nameText) + nameFS:SetPoint("LEFT", classIcon, "RIGHT", 2, 0) + nameFS:SetPoint("RIGHT", slot, "RIGHT", -2, 0) + slot.nameFS = nameFS + + slot.infoFS = nil + + slot:RegisterForClicks("LeftButtonUp", "RightButtonUp") + slot:RegisterForDrag("LeftButton") + + slot:SetScript("OnClick", function() + if not this.raidIndex then return end + if arg1 == "RightButton" then + SUI:ShowRaidSlotMenu(this) + end + end) + + slot:SetScript("OnDragStart", function() + if not this.raidIndex then return end + if not (IsRaidLeader() or IsRaidOfficer()) then return end + dragSlot = this + if not dragHighlight then + dragHighlight = CreateFrame("Frame", "SFramesRaidDragHL", UIParent) + dragHighlight:SetWidth(RAID_SLOT_W) + dragHighlight:SetHeight(RAID_SLOT_H) + dragHighlight:SetFrameStrata("TOOLTIP") + SetPixelBackdrop(dragHighlight, T.tabActiveBg, T.tabActiveBorder) + local dfs = MakeFS(dragHighlight, 8, "CENTER", T.tabActiveText) + dfs:SetPoint("CENTER", 0, 0) + dragHighlight.text = dfs + end + dragHighlight.text:SetText(this.playerName or "") + local cc = GetClassColor(this.playerClass) + dragHighlight.text:SetTextColor(cc[1], cc[2], cc[3]) + dragHighlight:Show() + dragHighlight:SetScript("OnUpdate", function() + local cx, cy = GetCursorPosition() + local s = UIParent:GetEffectiveScale() + dragHighlight:ClearAllPoints() + dragHighlight:SetPoint("CENTER", UIParent, "BOTTOMLEFT", cx / s, cy / s) + end) + end) + + slot:SetScript("OnDragStop", function() + if not dragSlot then return end + if dragHighlight then + dragHighlight:Hide() + dragHighlight:SetScript("OnUpdate", nil) + end + local cx, cy = GetCursorPosition() + local es = UIParent:GetEffectiveScale() + cx = cx / es + cy = cy / es + local targetSlot = nil + local targetGroup = nil + for tg = 1, RAID_GROUPS do + for ts = 1, RAID_COLS do + local tSlot = raidSlots[tg] and raidSlots[tg][ts] + if tSlot and tSlot:IsVisible() then + local left = tSlot:GetLeft() + local right = tSlot:GetRight() + local top = tSlot:GetTop() + local bottom = tSlot:GetBottom() + if left and cx >= left and cx <= right and cy >= bottom and cy <= top then + targetSlot = tSlot + targetGroup = tg + break + end + end + end + if targetGroup then break end + end + if not targetGroup then + for tg = 1, RAID_GROUPS do + local gf = raidSlots[tg] and raidSlots[tg][1] and raidSlots[tg][1]:GetParent() + if gf and gf:IsVisible() then + local left = gf:GetLeft() + local right = gf:GetRight() + local top = gf:GetTop() + local bottom = gf:GetBottom() + if left and cx >= left and cx <= right and cy >= bottom and cy <= top then + targetGroup = tg + break + end + end + end + end + if targetGroup and dragSlot.raidIndex and targetGroup ~= dragSlot.group then + if targetSlot and targetSlot.raidIndex and SwapRaidMembers then + SwapRaidMembers(dragSlot.raidIndex, targetSlot.raidIndex) + else + SetRaidSubgroup(dragSlot.raidIndex, targetGroup) + end + end + dragSlot = nil + end) + + slot:SetScript("OnEnter", function() + if dragSlot and dragSlot ~= this then + SetPixelBackdrop(this, { 0.30, 0.18, 0.26, 0.9 }, T.tabActiveBorder) + else + SetPixelBackdrop(this, T.slotHover, T.slotSelected) + end + if this.playerName and not dragSlot then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:AddLine(this.playerName, 1, 1, 1) + if this.playerClass then + local cc = GetClassColor(this.playerClass) + GameTooltip:AddLine(this.playerClass, cc[1], cc[2], cc[3]) + end + if this.playerLevel then + GameTooltip:AddLine("等级 " .. this.playerLevel, T.bodyText[1], T.bodyText[2], T.bodyText[3]) + end + if IsRaidLeader() or IsRaidOfficer() then + GameTooltip:AddLine("左键拖拽: 移动到其他小组", T.dimText[1], T.dimText[2], T.dimText[3]) + end + GameTooltip:AddLine("右键: 团队操作菜单", T.dimText[1], T.dimText[2], T.dimText[3]) + GameTooltip:Show() + end + end) + slot:SetScript("OnLeave", function() + if this.raidIndex then + SetPixelBackdrop(this, T.slotBg, T.slotBorder) + else + SetPixelBackdrop(this, T.raidSlotEmpty, T.slotBorder) + end + GameTooltip:Hide() + end) + + if not raidSlots[g] then raidSlots[g] = {} end + raidSlots[g][s] = slot + end + end + + rScroll:SetContentHeight(RAID_GROUPS * (groupH + RAID_GROUP_PAD)) + + local btnBar = CreateFrame("Frame", nil, page) + btnBar:SetHeight(BOTTOM_H) + btnBar:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0) + btnBar:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) + + local convertBtn = MakeButton(btnBar, "转换团队", 80, 22) + convertBtn:SetPoint("BOTTOMLEFT", btnBar, "BOTTOMLEFT", 0, 2) + convertBtn:SetScript("OnClick", function() + if GetNumRaidMembers() == 0 and GetNumPartyMembers() > 0 then + ConvertToRaid() + end + end) + + local leaveBtn = MakeButton(btnBar, "离开团队", 80, 22) + leaveBtn:SetPoint("LEFT", convertBtn, "RIGHT", 4, 0) + leaveBtn:SetScript("OnClick", function() + if GetNumRaidMembers() > 0 then LeaveParty() end + end) + + local readyBtn = MakeButton(btnBar, "就位确认", 80, 22) + readyBtn:SetPoint("LEFT", leaveBtn, "RIGHT", 4, 0) + readyBtn:SetScript("OnClick", function() + if DoReadyCheck then DoReadyCheck() end + end) +end + +function SUI:GetRaidUnitByName(targetName) + for i = 1, 40 do + local u = "raid" .. i + if UnitExists(u) and UnitName(u) == targetName then + return u + end + end + return nil +end + +function SUI:ShowRaidSlotMenu(slot) + if not slot.raidIndex then return end + local pName = slot.playerName + if not pName then return end + + if not SUI.raidDropDown then + SUI.raidDropDown = CreateFrame("Frame", "SFramesSocialRaidDD", UIParent, "UIDropDownMenuTemplate") + SUI.raidDropDown.displayMode = "MENU" + SUI.raidDropDown.initialize = function() + local rd = SUI.raidDropDown + local name = rd.slotName + local rIdx = rd.slotRaidIndex + local grp = rd.slotGroup + local rnk = rd.slotRank + local isSelf = rd.slotIsSelf + if not name or not rIdx then return end + + local isLeader = IsRaidLeader() + local isOfficer = IsRaidOfficer() + local info + + info = {} + info.text = name + info.isTitle = 1 + info.notCheckable = 1 + UIDropDownMenu_AddButton(info) + + if not isSelf then + info = {} + info.text = "密语" + info.notCheckable = 1 + info.func = function() ChatFrame_SendTell(name) end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = "观察" + info.notCheckable = 1 + info.func = function() + local u = SUI:GetRaidUnitByName(name) + if u then InspectUnit(u) end + end + UIDropDownMenu_AddButton(info) + end + + if isLeader or isOfficer then + for gIdx = 1, RAID_GROUPS do + if gIdx ~= grp then + info = {} + info.text = "移至小组 " .. gIdx + info.notCheckable = 1 + local tg = gIdx + info.func = function() SetRaidSubgroup(rIdx, tg) end + UIDropDownMenu_AddButton(info) + end + end + end + + if isLeader and not isSelf then + if rnk and rnk < 1 then + info = {} + info.text = "提升为助理" + info.notCheckable = 1 + info.func = function() PromoteToAssistant(name) end + UIDropDownMenu_AddButton(info) + elseif rnk and rnk == 1 then + info = {} + info.text = "取消助理" + info.notCheckable = 1 + info.func = function() DemoteAssistant(name) end + UIDropDownMenu_AddButton(info) + end + + info = {} + info.text = "转让队长" + info.notCheckable = 1 + info.func = function() + StaticPopupDialogs["SFRAMES_PROMOTE_LEADER"] = { + text = "确定要将队长转让给 " .. name .. " 吗?", + button1 = "确定", + button2 = "取消", + OnAccept = function() PromoteToLeader(name) end, + timeout = 0, + whileDead = true, + hideOnEscape = true, + } + StaticPopup_Show("SFRAMES_PROMOTE_LEADER") + end + UIDropDownMenu_AddButton(info) + end + + if (isLeader or isOfficer) and not isSelf then + info = {} + info.text = "移出团队" + info.notCheckable = 1 + info.func = function() UninviteByName(name) end + UIDropDownMenu_AddButton(info) + end + + info = {} + info.text = "取消" + info.notCheckable = 1 + UIDropDownMenu_AddButton(info) + end + end + + SUI.raidDropDown.slotName = pName + SUI.raidDropDown.slotRaidIndex = slot.raidIndex + SUI.raidDropDown.slotGroup = slot.group + SUI.raidDropDown.slotRank = slot.playerRank + SUI.raidDropDown.slotIsSelf = (pName == UnitName("player")) + ToggleDropDownMenu(1, nil, SUI.raidDropDown, "cursor") +end + +local function CancelRaidDrag() + if dragSlot then + dragSlot = nil + if dragHighlight then + dragHighlight:Hide() + dragHighlight:SetScript("OnUpdate", nil) + end + end +end + +function SUI:UpdateRaidPage() + local page = pages[4] + if not page then return end + + local numRaid = GetNumRaidMembers() + + -- Clear all slots + for g = 1, RAID_GROUPS do + for s = 1, RAID_COLS do + local slot = raidSlots[g] and raidSlots[g][s] + if slot then + slot.raidIndex = nil + slot.playerName = nil + slot.playerClass = nil + slot.playerRank = nil + slot.playerLevel = nil + slot.nameFS:SetText("") + slot.classIcon:Hide() + SetPixelBackdrop(slot, T.raidSlotEmpty, T.slotBorder) + end + end + end + + if numRaid == 0 then + page.raidInfoFS:SetText("你不在团队中") + return + end + + page.raidInfoFS:SetText("团队成员: " .. numRaid .. "/40") + + local groupCount = {} + for g = 1, RAID_GROUPS do groupCount[g] = 0 end + + for i = 1, numRaid do + local name, rank, subgroup, level, class, fileName, zone, online, isDead, role, isML = GetRaidRosterInfo(i) + if name and subgroup then + local g = subgroup + groupCount[g] = (groupCount[g] or 0) + 1 + local s = groupCount[g] + if s <= RAID_COLS then + local slot = raidSlots[g] and raidSlots[g][s] + if slot then + slot.raidIndex = i + slot.playerName = name + slot.playerClass = fileName + slot.playerRank = rank + slot.playerLevel = level + + local shortName = name + if string.len(name) > 7 then + shortName = string.sub(name, 1, 6) .. ".." + end + local prefix = "" + if rank and rank == 2 then prefix = "*" + elseif rank and rank == 1 then prefix = "+" end + slot.nameFS:SetText(prefix .. shortName) + + local cc = GetClassColor(fileName) + slot.nameFS:SetTextColor(cc[1], cc[2], cc[3]) + SetClassIcon(slot.classIcon, fileName) + + if online then + SetPixelBackdrop(slot, T.slotBg, T.slotBorder) + if isDead then + slot.nameFS:SetTextColor(0.5, 0.2, 0.2) + end + else + SetPixelBackdrop(slot, { 0.06, 0.03, 0.05, 0.7 }, { 0.20, 0.15, 0.18, 0.6 }) + slot.nameFS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) + end + end + end + end + end +end + +-------------------------------------------------------------------------------- +-- Main Tab Switching +-------------------------------------------------------------------------------- +local TAB_NAMES = { "好友", "查询", "工会", "团队" } + +local function ShowPage(tabIdx) + currentMainTab = tabIdx + for i = 1, 4 do + local p = pages[i] + if p then + if i == tabIdx then p:Show() else p:Hide() end + end + local btn = mainTabs[i] + if btn then + if i == tabIdx then + SetRoundBackdrop(btn, T.tabActiveBg, T.tabActiveBorder) + btn.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + else + SetRoundBackdrop(btn, T.tabBg, T.tabBorder) + btn.text:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + end + end + end + + if tabIdx == 1 then + if origShowFriendsAPI then origShowFriendsAPI() end + SUI:UpdateFriendsPage() + elseif tabIdx == 2 then + SUI:UpdateWhoList() + elseif tabIdx == 3 then + SUI:UpdateGuildList() + elseif tabIdx == 4 then + SUI:UpdateRaidPage() + end + + if MainFrame and MainFrame.titleFS then + local titles = { "好友名单", "查询玩家", "公会", "团队" } + MainFrame.titleFS:SetText(titles[tabIdx] or "社交") + end +end + +-------------------------------------------------------------------------------- +-- Build Main Frame +-------------------------------------------------------------------------------- +local function BuildMainFrame() + if MainFrame then return end + + local f = CreateFrame("Frame", "SFramesSocialUI", UIParent) + f:SetWidth(FRAME_W) + f:SetHeight(FRAME_H) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + f:SetFrameStrata("HIGH") + f:SetFrameLevel(10) + SetRoundBackdrop(f) + CreateShadow(f) + + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + f:SetScript("OnHide", function() CancelRaidDrag() end) + + -- Header + local header = CreateFrame("Frame", nil, f) + header:SetHeight(HEADER_H) + header:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) + header:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0) + + local titleIco = SFrames:CreateIcon(header, "friends", 14) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("CENTER", header, "CENTER", -40, 0) + titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) + + local title = MakeFS(header, 13, "CENTER", T.gold) + title:SetPoint("LEFT", titleIco, "RIGHT", 4, 0) + title:SetText("好友名单") + f.titleFS = title + + local closeBtn = CreateFrame("Button", NextName("Close"), header) + closeBtn:SetWidth(20) + closeBtn:SetHeight(20) + closeBtn:SetPoint("RIGHT", header, "RIGHT", -6, 0) + closeBtn:SetScript("OnClick", function() f:Hide() end) + local closeIco = SFrames:CreateIcon(closeBtn, "close", 12) + closeIco:SetDrawLayer("OVERLAY") + closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) + closeIco:SetVertexColor(1, 0.7, 0.7) + closeBtn:SetScript("OnEnter", function() + closeIco:SetVertexColor(1, 0.4, 0.5) + end) + closeBtn:SetScript("OnLeave", function() + closeIco:SetVertexColor(1, 0.7, 0.7) + end) + + -- Main tabs (use this.tabIndex to avoid closure issues) + local tabContainer = CreateFrame("Frame", nil, f) + tabContainer:SetHeight(TAB_BAR_H) + tabContainer:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD, -(HEADER_H + 2)) + tabContainer:SetPoint("TOPRIGHT", f, "TOPRIGHT", -SIDE_PAD, -(HEADER_H + 2)) + + local tabW = math.floor((CONTENT_W - 6) / 4) + for idx = 1, 4 do + local tabIdx = idx + local btn = CreateFrame("Button", NextName("MTab"), tabContainer) + btn:SetWidth(tabW) + btn:SetHeight(TAB_BAR_H - 2) + btn.tabIndex = tabIdx + if tabIdx == 1 then + btn:SetPoint("LEFT", tabContainer, "LEFT", 0, 0) + else + btn:SetPoint("LEFT", mainTabs[tabIdx - 1], "RIGHT", 2, 0) + end + SetRoundBackdrop(btn, T.tabBg, T.tabBorder) + local text = MakeFS(btn, 10, "CENTER", T.tabText) + text:SetPoint("CENTER", 0, 0) + text:SetText(TAB_NAMES[tabIdx]) + btn.text = text + btn:SetScript("OnClick", function() + ShowPage(this.tabIndex) + end) + mainTabs[tabIdx] = btn + end + + -- Content pages + local contentTop = -(HEADER_H + TAB_BAR_H + 6) + for i = 1, 4 do + local page = CreateFrame("Frame", nil, f) + page:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD, contentTop) + page:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -SIDE_PAD, SIDE_PAD) + page:Hide() + pages[i] = page + end + + BuildFriendsPage(pages[1]) + BuildWhoPage(pages[2]) + BuildGuildPage(pages[3]) + BuildRaidPage(pages[4]) + + table.insert(UISpecialFrames, "SFramesSocialUI") + + local scale = SFramesDB.socialScale or 1 + if scale ~= 1 then f:SetScale(scale) end + + MainFrame = f + f:Hide() +end + +-------------------------------------------------------------------------------- +-- Public API +-------------------------------------------------------------------------------- +function SUI:Toggle(tabIdx) + if not initialized then return end + if not MainFrame then BuildMainFrame() end + + if MainFrame:IsShown() then + if tabIdx and tabIdx ~= currentMainTab then + ShowPage(tabIdx) + else + MainFrame:Hide() + end + else + MainFrame:Show() + ShowPage(tabIdx or 1) + end +end + +function SUI:Show(tabIdx) + if not initialized then return end + if not MainFrame then BuildMainFrame() end + MainFrame:Show() + ShowPage(tabIdx or 1) +end + +function SUI:Hide() + if MainFrame then MainFrame:Hide() end +end + +function SUI:IsShown() + return MainFrame and MainFrame:IsShown() +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function SUI:Initialize() + if initialized then return end + initialized = true + + BuildClassReverseLookup() + HideBlizzardFriends() + BuildMainFrame() + + local ef = CreateFrame("Frame", nil, UIParent) + ef:RegisterEvent("FRIENDLIST_UPDATE") + ef:RegisterEvent("IGNORELIST_UPDATE") + ef:RegisterEvent("WHO_LIST_UPDATE") + ef:RegisterEvent("GUILD_ROSTER_UPDATE") + ef:RegisterEvent("RAID_ROSTER_UPDATE") + ef:RegisterEvent("PARTY_MEMBERS_CHANGED") + ef:SetScript("OnEvent", function() + if not MainFrame or not MainFrame:IsShown() then return end + if event == "FRIENDLIST_UPDATE" or event == "IGNORELIST_UPDATE" then + if currentMainTab == 1 then SUI:UpdateFriendsPage() end + elseif event == "WHO_LIST_UPDATE" then + if currentMainTab == 2 then SUI:UpdateWhoList() end + elseif event == "GUILD_ROSTER_UPDATE" then + if currentMainTab == 3 then SUI:UpdateGuildList() end + elseif event == "RAID_ROSTER_UPDATE" or event == "PARTY_MEMBERS_CHANGED" then + if currentMainTab == 4 then SUI:UpdateRaidPage() end + end + end) +end + +-------------------------------------------------------------------------------- +-- Hook ToggleFriendsFrame +-------------------------------------------------------------------------------- +local origToggleFriendsFrame = ToggleFriendsFrame +ToggleFriendsFrame = function(tab) + if SFramesDB and SFramesDB.enableSocial == false then + if origToggleFriendsFrame then + if origFriendsFrameShow and FriendsFrame then + FriendsFrame.Show = origFriendsFrameShow + end + origToggleFriendsFrame(tab) + end + return + end + SUI:Toggle(tab or 1) +end + +if ShowFriends then + origShowFriendsAPI = ShowFriends + local origShowFriends = ShowFriends + ShowFriends = function() + if SFramesDB and SFramesDB.enableSocial == false then + if origShowFriends then origShowFriends() end + return + end + SUI:Show(1) + end +end + +-------------------------------------------------------------------------------- +-- Bootstrap +-------------------------------------------------------------------------------- +local bootstrap = CreateFrame("Frame", nil, UIParent) +bootstrap:RegisterEvent("PLAYER_LOGIN") +bootstrap:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + if SFramesDB.enableSocial == nil then + SFramesDB.enableSocial = true + end + if SFramesDB.enableSocial ~= false then + SUI:Initialize() + end + end +end) diff --git a/SpellBookUI.lua b/SpellBookUI.lua new file mode 100644 index 0000000..cce98fe --- /dev/null +++ b/SpellBookUI.lua @@ -0,0 +1,983 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: SpellBook UI (SpellBookUI.lua) +-- Replaces the default SpellBookFrame with a modern rounded UI +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.SpellBookUI = {} +local SB = SFrames.SpellBookUI +SFramesDB = SFramesDB or {} + +-------------------------------------------------------------------------------- +-- Theme (aligned with CharacterPanel / SocialUI standard palette) +-------------------------------------------------------------------------------- +local T = SFrames.ActiveTheme + +-------------------------------------------------------------------------------- +-- Layout (single table to stay under upvalue limit) +-------------------------------------------------------------------------------- +local L = { + RIGHT_TAB_W = 56, + SIDE_PAD = 10, + CONTENT_W = 316, + HEADER_H = 30, + SPELL_COLS = 2, + SPELL_ROWS = 8, + SPELL_H = 38, + ICON_SIZE = 30, + PAGE_H = 26, + OPTIONS_H = 26, + TAB_H = 40, + TAB_GAP = 2, + TAB_ICON = 26, + BOOK_TAB_W = 52, + BOOK_TAB_H = 22, +} +L.SPELLS_PER_PAGE = L.SPELL_COLS * L.SPELL_ROWS +L.SPELL_W = (L.CONTENT_W - 4) / L.SPELL_COLS +L.MAIN_W = L.CONTENT_W + L.SIDE_PAD * 2 +L.FRAME_W = L.MAIN_W + L.RIGHT_TAB_W + 4 +L.FRAME_H = L.HEADER_H + 6 + L.SPELL_ROWS * L.SPELL_H + 6 + L.PAGE_H + 4 + L.OPTIONS_H + 10 + +-------------------------------------------------------------------------------- +-- State (single table to stay under upvalue limit) +-------------------------------------------------------------------------------- +local S = { + frame = nil, + spellButtons = {}, + tabButtons = {}, + bookTabs = {}, + currentTab = 1, + currentPage = 1, + currentBook = "spell", + initialized = false, + filteredCache = nil, + scanTip = nil, +} + +local widgetId = 0 +local function NextName(p) + widgetId = widgetId + 1 + return "SFramesSB" .. (p or "") .. tostring(widgetId) +end + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + local bg = bgColor or T.panelBg + local bd = borderColor or T.panelBorder + frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) + frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) +end + +local function SetPixelBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + if bgColor then + frame:SetBackdropColor(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1) + end + if borderColor then + frame:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1) + end +end + +local function CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0)) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.6) + s:SetBackdropBorderColor(0, 0, 0, 0.45) + return s +end + +local function MakeFS(parent, size, justifyH, color) + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), size or 11, "OUTLINE") + fs:SetJustifyH(justifyH or "LEFT") + local c = color or T.nameText + fs:SetTextColor(c[1], c[2], c[3]) + return fs +end + +local function MakeButton(parent, text, w, h) + local btn = CreateFrame("Button", NextName("Btn"), parent) + btn:SetWidth(w or 80) + btn:SetHeight(h or 22) + SetRoundBackdrop(btn, T.btnBg, T.btnBorder) + local fs = MakeFS(btn, 10, "CENTER", T.btnText) + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text or "") + btn.text = fs + btn:SetScript("OnEnter", function() + SetRoundBackdrop(this, T.btnHoverBg, T.tabActiveBorder) + this.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + end) + btn:SetScript("OnLeave", function() + SetRoundBackdrop(this, T.btnBg, T.btnBorder) + this.text:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end) + return btn +end + +local function MakeSep(parent, y) + local sep = parent:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetHeight(1) + sep:SetPoint("TOPLEFT", parent, "TOPLEFT", 4, y) + sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4, y) + sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + return sep +end + +-------------------------------------------------------------------------------- +-- Hide Blizzard SpellBook +-------------------------------------------------------------------------------- +local function HideBlizzardSpellBook() + if not SpellBookFrame then return end + SpellBookFrame:SetAlpha(0) + SpellBookFrame:EnableMouse(false) + SpellBookFrame:ClearAllPoints() + SpellBookFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) + if SpellBookFrame.SetScript then + SpellBookFrame:SetScript("OnShow", function() this:Hide() end) + end +end + +-------------------------------------------------------------------------------- +-- Spell Data Helpers +-------------------------------------------------------------------------------- +local function GetBookType() + if S.currentBook == "pet" then return BOOKTYPE_PET or "pet" end + return BOOKTYPE_SPELL or "spell" +end + +local function GetTabInfo() + local numTabs = GetNumSpellTabs() + local tabs = {} + for i = 1, numTabs do + local name, texture, offset, numSpells = GetSpellTabInfo(i) + if name then + table.insert(tabs, { + name = name, texture = texture, + offset = offset, numSpells = numSpells, index = i, + }) + end + end + return tabs +end + +local function GetCurrentTabSpells() + local tabs = GetTabInfo() + local tab = tabs[S.currentTab] + if not tab then return 0, 0 end + return tab.offset, tab.numSpells +end + +local function GetFilteredSpellList() + local offset, numSpells = GetCurrentTabSpells() + local bookType = GetBookType() + + if not SFramesDB.spellBookHighestOnly then + local list = {} + for i = 1, numSpells do + table.insert(list, offset + i) + end + S.filteredCache = list + return list + end + + local seen = {} + local order = {} + for i = 1, numSpells do + local idx = offset + i + local name, rank = GetSpellName(idx, bookType) + if name then + if seen[name] then + for k = 1, table.getn(order) do + if order[k].name == name then + order[k].idx = idx + break + end + end + else + seen[name] = true + table.insert(order, { name = name, idx = idx }) + end + end + end + + local list = {} + for _, v in ipairs(order) do + table.insert(list, v.idx) + end + S.filteredCache = list + return list +end + +local function GetMaxPages() + local list = S.filteredCache or GetFilteredSpellList() + return math.max(1, math.ceil(table.getn(list) / L.SPELLS_PER_PAGE)) +end + +-------------------------------------------------------------------------------- +-- Auto-Replace Action Bar (lower rank -> highest rank) +-------------------------------------------------------------------------------- +local function EnsureScanTooltip() + if S.scanTip then return end + S.scanTip = CreateFrame("GameTooltip", "SFramesSBScanTip", UIParent, "GameTooltipTemplate") + S.scanTip:SetOwner(UIParent, "ANCHOR_NONE") + S.scanTip:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 1000, 1000) +end + +local function AutoReplaceActionBarSpells() + if not SFramesDB.spellBookAutoReplace then return end + if UnitAffectingCombat and UnitAffectingCombat("player") then return end + + EnsureScanTooltip() + + local highestByName = {} + local highestRankText = {} + local numTabs = GetNumSpellTabs() + for tab = 1, numTabs do + local _, _, offset, numSpells = GetSpellTabInfo(tab) + for i = 1, numSpells do + local idx = offset + i + local name, rank = GetSpellName(idx, "spell") + if name and not IsSpellPassive(idx, "spell") then + highestByName[name] = idx + highestRankText[name] = rank or "" + end + end + end + + local tipLeft = getglobal("SFramesSBScanTipTextLeft1") + local tipRight = getglobal("SFramesSBScanTipTextRight1") + + for slot = 1, 120 do + if HasAction(slot) then + S.scanTip:ClearLines() + S.scanTip:SetAction(slot) + local actionName = tipLeft and tipLeft:GetText() + local actionRank = tipRight and tipRight:GetText() + if actionName and highestByName[actionName] then + local bestRank = highestRankText[actionName] + if bestRank and bestRank ~= "" and actionRank and actionRank ~= bestRank then + PickupSpell(highestByName[actionName], "spell") + PlaceAction(slot) + ClearCursor() + end + end + end + end +end + +-------------------------------------------------------------------------------- +-- Slot backdrop helper: backdrop on a SEPARATE child at lower frameLevel +-- so icon / text render cleanly above it (same fix as ActionBars) +-------------------------------------------------------------------------------- +local function SetSlotBg(btn, bgColor, borderColor) + if not btn.sfBg then return end + SetPixelBackdrop(btn.sfBg, bgColor or T.slotBg, borderColor or T.slotBorder) +end + +local function CreateSlotBackdrop(btn) + if btn:GetBackdrop() then btn:SetBackdrop(nil) end + local level = btn:GetFrameLevel() + local bd = CreateFrame("Frame", nil, btn) + bd:SetFrameLevel(level > 0 and (level - 1) or 0) + bd:SetAllPoints(btn) + SetPixelBackdrop(bd, T.slotBg, T.slotBorder) + btn.sfBg = bd + return bd +end + +-------------------------------------------------------------------------------- +-- Update Spell Buttons +-------------------------------------------------------------------------------- +local function UpdateSpellButtons() + if not S.frame or not S.frame:IsShown() then return end + + local list = GetFilteredSpellList() + local bookType = GetBookType() + local startIdx = (S.currentPage - 1) * L.SPELLS_PER_PAGE + local totalSpells = table.getn(list) + + for i = 1, L.SPELLS_PER_PAGE do + local btn = S.spellButtons[i] + if not btn then break end + + local listIdx = startIdx + i + local spellIdx = list[listIdx] + + if spellIdx and listIdx <= totalSpells then + local spellName, spellRank = GetSpellName(spellIdx, bookType) + local texture = GetSpellTexture(spellIdx, bookType) + + btn.icon:SetTexture(texture) + btn.icon:SetAlpha(1) + btn.nameFS:SetText(spellName or "") + btn.subFS:SetText(spellRank or "") + btn.spellId = spellIdx + btn.bookType = bookType + + local isPassive = IsSpellPassive(spellIdx, bookType) + if isPassive then + btn.nameFS:SetTextColor(T.passive[1], T.passive[2], T.passive[3]) + btn.subFS:SetTextColor(T.passive[1], T.passive[2], T.passive[3]) + btn.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3]) + btn.passiveBadge:Show() + else + btn.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + btn.subFS:SetTextColor(T.subText[1], T.subText[2], T.subText[3]) + btn.icon:SetVertexColor(1, 1, 1) + btn.passiveBadge:Hide() + end + + SetSlotBg(btn, T.slotBg, T.slotBorder) + btn:Show() + btn:Enable() + else + btn.icon:SetTexture(nil) + btn.icon:SetAlpha(0) + btn.nameFS:SetText("") + btn.subFS:SetText("") + btn.spellId = nil + btn.bookType = nil + btn.passiveBadge:Hide() + SetSlotBg(btn, T.emptySlotBg, T.emptySlotBd) + btn:Show() + btn:Disable() + end + end + + local maxPages = GetMaxPages() + local f = S.frame + if f.pageText then + if maxPages <= 1 then + f.pageText:SetText("") + else + f.pageText:SetText(S.currentPage .. " / " .. maxPages) + end + end + if f.prevBtn then + if S.currentPage > 1 then + f.prevBtn:Enable(); f.prevBtn:SetAlpha(1) + else + f.prevBtn:Disable(); f.prevBtn:SetAlpha(0.4) + end + end + if f.nextBtn then + if S.currentPage < maxPages then + f.nextBtn:Enable(); f.nextBtn:SetAlpha(1) + else + f.nextBtn:Disable(); f.nextBtn:SetAlpha(0.4) + end + end +end + +-------------------------------------------------------------------------------- +-- Update Skill Line Tabs (right side) +-------------------------------------------------------------------------------- +local function UpdateSkillLineTabs() + local tabs = GetTabInfo() + for i = 1, 8 do + local btn = S.tabButtons[i] + if not btn then break end + local tab = tabs[i] + if tab then + if tab.texture then + btn.tabIcon:SetTexture(tab.texture) + btn.tabIcon:Show() + else + btn.tabIcon:Hide() + end + btn.tabLabel:SetText(tab.name or "") + btn:Show() + + if i == S.currentTab then + SetRoundBackdrop(btn, T.tabActiveBg, T.tabActiveBorder) + btn.tabIcon:SetVertexColor(1, 1, 1) + btn.tabLabel:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + btn.indicator:Show() + else + SetRoundBackdrop(btn, T.tabBg, T.tabBorder) + btn.tabIcon:SetVertexColor(T.tabText[1], T.tabText[2], T.tabText[3]) + btn.tabLabel:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + btn.indicator:Hide() + end + else + btn:Hide() + end + end +end + +-------------------------------------------------------------------------------- +-- Update Book Tabs (Spell / Pet) +-------------------------------------------------------------------------------- +local function UpdateBookTabs() + for i = 1, 2 do + local bt = S.bookTabs[i] + if not bt then break end + local isActive = (i == 1 and S.currentBook == "spell") or (i == 2 and S.currentBook == "pet") + if isActive then + SetRoundBackdrop(bt, T.tabActiveBg, T.tabActiveBorder) + bt.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + else + SetRoundBackdrop(bt, T.tabBg, T.tabBorder) + bt.text:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + end + end + local petTab = S.bookTabs[2] + if petTab then + local hasPet = HasPetSpells and HasPetSpells() + if hasPet then petTab:Show() else petTab:Hide() end + end +end + +local function FullRefresh() + S.filteredCache = nil + UpdateSkillLineTabs() + UpdateBookTabs() + UpdateSpellButtons() + if S.frame and S.frame.optHighest then + S.frame.optHighest:UpdateVisual() + end + if S.frame and S.frame.optReplace then + S.frame.optReplace:UpdateVisual() + end +end + +-------------------------------------------------------------------------------- +-- Build: Header +-------------------------------------------------------------------------------- +local function BuildHeader(f) + local header = CreateFrame("Frame", nil, f) + header:SetHeight(L.HEADER_H) + header:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) + header:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0) + + local function MakeBookTab(parent, label, idx, xOff) + local bt = CreateFrame("Button", NextName("BookTab"), parent) + bt:SetWidth(L.BOOK_TAB_W) + bt:SetHeight(L.BOOK_TAB_H) + bt:SetPoint("LEFT", parent, "LEFT", xOff, 0) + SetRoundBackdrop(bt, T.tabBg, T.tabBorder) + + local txt = MakeFS(bt, 10, "CENTER", T.tabText) + txt:SetPoint("CENTER", 0, 0) + txt:SetText(label) + bt.text = txt + bt.bookIdx = idx + + bt:SetScript("OnClick", function() + if this.bookIdx == 1 then S.currentBook = "spell" else S.currentBook = "pet" end + S.currentTab = 1 + S.currentPage = 1 + FullRefresh() + end) + bt:SetScript("OnEnter", function() + this.text:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + end) + bt:SetScript("OnLeave", function() + local active = (this.bookIdx == 1 and S.currentBook == "spell") or (this.bookIdx == 2 and S.currentBook == "pet") + if active then + this.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + else + this.text:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + end + end) + return bt + end + + S.bookTabs[1] = MakeBookTab(header, "法术", 1, 8) + S.bookTabs[2] = MakeBookTab(header, "宠物", 2, 8 + L.BOOK_TAB_W + 4) + + local titleIco = SFrames:CreateIcon(header, "spellbook", 14) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("CENTER", header, "CENTER", -28, 0) + titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) + + local title = MakeFS(header, 13, "CENTER", T.gold) + title:SetPoint("LEFT", titleIco, "RIGHT", 4, 0) + title:SetText("法术书") + f.titleFS = title + + local closeBtn = CreateFrame("Button", NextName("Close"), header) + closeBtn:SetWidth(20) + closeBtn:SetHeight(20) + closeBtn:SetPoint("RIGHT", header, "RIGHT", -8, 0) + SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder) + closeBtn:SetScript("OnClick", function() this:GetParent():GetParent():Hide() end) + local closeIco = SFrames:CreateIcon(closeBtn, "close", 12) + closeIco:SetDrawLayer("OVERLAY") + closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) + closeIco:SetVertexColor(1, 0.7, 0.7) + closeBtn.nanamiIcon = closeIco + closeBtn:SetScript("OnEnter", function() + SetRoundBackdrop(this, T.btnHoverBg, T.btnHoverBd) + if this.nanamiIcon then this.nanamiIcon:SetVertexColor(1, 1, 1) end + end) + closeBtn:SetScript("OnLeave", function() + SetRoundBackdrop(this, T.buttonDownBg, T.btnBorder) + if this.nanamiIcon then this.nanamiIcon:SetVertexColor(1, 0.7, 0.7) end + end) + + MakeSep(f, -L.HEADER_H) +end + +-------------------------------------------------------------------------------- +-- Build: Right Skill Line Tabs +-------------------------------------------------------------------------------- +local function BuildSkillTabs(f) + for idx = 1, 8 do + local btn = CreateFrame("Button", NextName("Tab"), f) + btn:SetWidth(L.RIGHT_TAB_W) + btn:SetHeight(L.TAB_H) + btn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -3, + -(L.HEADER_H + 4 + (idx - 1) * (L.TAB_H + L.TAB_GAP))) + SetRoundBackdrop(btn, T.tabBg, T.tabBorder) + btn.tabIndex = idx + + local indicator = btn:CreateTexture(nil, "OVERLAY") + indicator:SetTexture("Interface\\Buttons\\WHITE8X8") + indicator:SetWidth(3) + indicator:SetPoint("TOPLEFT", btn, "TOPLEFT", 1, -4) + indicator:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", 1, 4) + indicator:SetVertexColor(T.accent[1], T.accent[2], T.accent[3], T.accent[4]) + indicator:Hide() + btn.indicator = indicator + + local icon = btn:CreateTexture(nil, "ARTWORK") + icon:SetWidth(L.TAB_ICON) + icon:SetHeight(L.TAB_ICON) + icon:SetPoint("TOP", btn, "TOP", 0, -4) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + btn.tabIcon = icon + + local label = MakeFS(btn, 7, "CENTER", T.tabText) + label:SetPoint("BOTTOM", btn, "BOTTOM", 0, 3) + label:SetWidth(L.RIGHT_TAB_W - 6) + btn.tabLabel = label + + btn:SetScript("OnClick", function() + S.currentTab = this.tabIndex + S.currentPage = 1 + FullRefresh() + end) + + btn:SetScript("OnEnter", function() + if this.tabIndex ~= S.currentTab then + SetRoundBackdrop(this, T.slotHover, T.tabActiveBorder) + this.tabIcon:SetVertexColor(1, 1, 1) + this.tabLabel:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + end + local tabs = GetTabInfo() + local tab = tabs[this.tabIndex] + if tab then + GameTooltip:SetOwner(this, "ANCHOR_LEFT") + GameTooltip:AddLine(tab.name, T.gold[1], T.gold[2], T.gold[3]) + GameTooltip:AddLine(tab.numSpells .. " 个法术", T.subText[1], T.subText[2], T.subText[3]) + GameTooltip:Show() + end + end) + btn:SetScript("OnLeave", function() + GameTooltip:Hide() + if this.tabIndex ~= S.currentTab then + SetRoundBackdrop(this, T.tabBg, T.tabBorder) + this.tabIcon:SetVertexColor(T.tabText[1], T.tabText[2], T.tabText[3]) + this.tabLabel:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + end + end) + + btn:Hide() + S.tabButtons[idx] = btn + end + + local tabSep = f:CreateTexture(nil, "ARTWORK") + tabSep:SetTexture("Interface\\Buttons\\WHITE8X8") + tabSep:SetWidth(1) + tabSep:SetPoint("TOPLEFT", f, "TOPRIGHT", -(L.RIGHT_TAB_W + 5), -(L.HEADER_H + 2)) + tabSep:SetPoint("BOTTOMLEFT", f, "BOTTOMRIGHT", -(L.RIGHT_TAB_W + 5), 4) + tabSep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) +end + +-------------------------------------------------------------------------------- +-- Build: Spell Buttons Grid +-- Backdrop on a SEPARATE child frame at lower frameLevel (ActionBars fix) +-------------------------------------------------------------------------------- +local function BuildSpellGrid(f) + local contentTop = -(L.HEADER_H + 6) + local contentFrame = CreateFrame("Frame", nil, f) + contentFrame:SetPoint("TOPLEFT", f, "TOPLEFT", L.SIDE_PAD, contentTop) + contentFrame:SetWidth(L.CONTENT_W) + contentFrame:SetHeight(L.SPELL_ROWS * L.SPELL_H + 4) + + for row = 1, L.SPELL_ROWS do + for col = 1, L.SPELL_COLS do + local idx = (row - 1) * L.SPELL_COLS + col + local btn = CreateFrame("Button", NextName("Spell"), contentFrame) + btn:SetWidth(L.SPELL_W - 2) + btn:SetHeight(L.SPELL_H - 2) + local x = (col - 1) * L.SPELL_W + 1 + local y = -((row - 1) * L.SPELL_H) + btn:SetPoint("TOPLEFT", contentFrame, "TOPLEFT", x, y) + + CreateSlotBackdrop(btn) + + local icon = btn:CreateTexture(nil, "ARTWORK") + icon:SetWidth(L.ICON_SIZE) + icon:SetHeight(L.ICON_SIZE) + icon:SetPoint("LEFT", btn, "LEFT", 5, 0) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + btn.icon = icon + + local nameFS = MakeFS(btn, 11, "LEFT", T.nameText) + nameFS:SetPoint("TOPLEFT", icon, "TOPRIGHT", 6, -2) + nameFS:SetPoint("RIGHT", btn, "RIGHT", -4, 0) + btn.nameFS = nameFS + + local subFS = MakeFS(btn, 9, "LEFT", T.subText) + subFS:SetPoint("BOTTOMLEFT", icon, "BOTTOMRIGHT", 6, 2) + subFS:SetPoint("RIGHT", btn, "RIGHT", -4, 0) + btn.subFS = subFS + + local passiveBadge = MakeFS(btn, 7, "RIGHT", T.passive) + passiveBadge:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -4, -3) + passiveBadge:SetText("被动") + passiveBadge:Hide() + btn.passiveBadge = passiveBadge + + btn:RegisterForClicks("LeftButtonUp", "RightButtonUp") + btn:RegisterForDrag("LeftButton") + + btn:SetScript("OnClick", function() + if not this.spellId then return end + if arg1 == "LeftButton" then + CastSpell(this.spellId, this.bookType) + elseif arg1 == "RightButton" then + PickupSpell(this.spellId, this.bookType) + end + end) + + btn:SetScript("OnDragStart", function() + if this.spellId then + PickupSpell(this.spellId, this.bookType) + end + end) + + btn:SetScript("OnEnter", function() + if this.spellId then + SetSlotBg(this, T.slotHover, T.slotSelected) + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetSpell(this.spellId, this.bookType) + GameTooltip:Show() + end + end) + + btn:SetScript("OnLeave", function() + if this.spellId then + SetSlotBg(this, T.slotBg, T.slotBorder) + else + SetSlotBg(this, T.emptySlotBg, T.emptySlotBd) + end + GameTooltip:Hide() + end) + + S.spellButtons[idx] = btn + end + end + + contentFrame:EnableMouseWheel(true) + contentFrame:SetScript("OnMouseWheel", function() + if arg1 > 0 then + if S.currentPage > 1 then + S.currentPage = S.currentPage - 1 + UpdateSpellButtons() + end + else + if S.currentPage < GetMaxPages() then + S.currentPage = S.currentPage + 1 + UpdateSpellButtons() + end + end + end) + + return contentTop +end + +-------------------------------------------------------------------------------- +-- Build: Pagination +-------------------------------------------------------------------------------- +local function BuildPagination(f, contentTop) + local pageY = contentTop - L.SPELL_ROWS * L.SPELL_H - 6 + + local prevBtn = MakeButton(f, "< 上一页", 66, L.PAGE_H) + prevBtn:SetPoint("TOPLEFT", f, "TOPLEFT", L.SIDE_PAD, pageY) + prevBtn:SetScript("OnClick", function() + if S.currentPage > 1 then + S.currentPage = S.currentPage - 1 + UpdateSpellButtons() + end + end) + f.prevBtn = prevBtn + + local nextBtn = MakeButton(f, "下一页 >", 66, L.PAGE_H) + nextBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -(L.RIGHT_TAB_W + L.SIDE_PAD + 4), pageY) + nextBtn:SetScript("OnClick", function() + if S.currentPage < GetMaxPages() then + S.currentPage = S.currentPage + 1 + UpdateSpellButtons() + end + end) + f.nextBtn = nextBtn + + local pageText = MakeFS(f, 11, "CENTER", T.pageText) + pageText:SetPoint("LEFT", prevBtn, "RIGHT", 4, 0) + pageText:SetPoint("RIGHT", nextBtn, "LEFT", -4, 0) + f.pageText = pageText + + return pageY +end + +-------------------------------------------------------------------------------- +-- Build: Options bar +-------------------------------------------------------------------------------- +local function BuildOptions(f, pageY) + local optY = pageY - L.PAGE_H - 4 + MakeSep(f, optY + 2) + + local function MakeCheckOption(parent, label, xOff, yOff, getFunc, setFunc) + local btn = CreateFrame("Button", NextName("Opt"), parent) + btn:SetHeight(L.OPTIONS_H - 4) + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", xOff, yOff) + + local box = CreateFrame("Frame", nil, btn) + box:SetWidth(12) + box:SetHeight(12) + box:SetPoint("LEFT", btn, "LEFT", 0, 0) + SetPixelBackdrop(box, T.checkOff, T.tabBorder) + btn.box = box + + local checkMark = MakeFS(box, 10, "CENTER", T.checkOn) + checkMark:SetPoint("CENTER", 0, 1) + checkMark:SetText("") + btn.checkMark = checkMark + + local txt = MakeFS(btn, 9, "LEFT", T.optionText) + txt:SetPoint("LEFT", box, "RIGHT", 4, 0) + txt:SetText(label) + btn.label = txt + + btn:SetWidth(txt:GetStringWidth() + 20) + btn.getFunc = getFunc + btn.setFunc = setFunc + + function btn:UpdateVisual() + if self.getFunc() then + self.checkMark:SetText("√") + SetPixelBackdrop(self.box, { T.checkOn[1]*0.3, T.checkOn[2]*0.3, T.checkOn[3]*0.3, 0.8 }, T.checkOn) + self.label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + else + self.checkMark:SetText("") + SetPixelBackdrop(self.box, T.checkOff, T.tabBorder) + self.label:SetTextColor(T.optionText[1], T.optionText[2], T.optionText[3]) + end + end + + btn:SetScript("OnClick", function() + this.setFunc(not this.getFunc()) + this:UpdateVisual() + FullRefresh() + end) + btn:SetScript("OnEnter", function() + this.label:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + end) + btn:SetScript("OnLeave", function() + this:UpdateVisual() + end) + + btn:UpdateVisual() + return btn + end + + f.optHighest = MakeCheckOption(f, "只显示最高等级", L.SIDE_PAD, optY, + function() return SFramesDB.spellBookHighestOnly == true end, + function(v) SFramesDB.spellBookHighestOnly = v; S.currentPage = 1 end + ) + + f.optReplace = MakeCheckOption(f, "学习新等级自动替换动作条", L.SIDE_PAD + 120, optY, + function() return SFramesDB.spellBookAutoReplace == true end, + function(v) SFramesDB.spellBookAutoReplace = v end + ) +end + +-------------------------------------------------------------------------------- +-- Build Main Frame +-------------------------------------------------------------------------------- +local function BuildMainFrame() + if S.frame then return end + + local f = CreateFrame("Frame", "SFramesSpellBookUI", UIParent) + f:SetWidth(L.FRAME_W) + f:SetHeight(L.FRAME_H) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + f:SetFrameStrata("HIGH") + f:SetFrameLevel(10) + SetRoundBackdrop(f) + CreateShadow(f) + + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + f:SetScript("OnShow", function() + if SpellBookFrame and SpellBookFrame:IsShown() then + SpellBookFrame:Hide() + end + FullRefresh() + end) + + BuildHeader(f) + BuildSkillTabs(f) + local contentTop = BuildSpellGrid(f) + local pageY = BuildPagination(f, contentTop) + BuildOptions(f, pageY) + + table.insert(UISpecialFrames, "SFramesSpellBookUI") + + local scale = SFramesDB.spellBookScale or 1 + if scale ~= 1 then f:SetScale(scale) end + + S.frame = f + f:Hide() +end + +-------------------------------------------------------------------------------- +-- Public API +-------------------------------------------------------------------------------- +function SB:Toggle(bookType) + if not S.initialized then return end + if not S.frame then BuildMainFrame() end + + if bookType then + if bookType == (BOOKTYPE_PET or "pet") then + S.currentBook = "pet" + else + S.currentBook = "spell" + end + end + + if S.frame:IsShown() then + S.frame:Hide() + else + S.currentTab = 1 + S.currentPage = 1 + S.frame:Show() + end +end + +function SB:Show(bookType) + if not S.initialized then return end + if not S.frame then BuildMainFrame() end + if bookType then + if bookType == (BOOKTYPE_PET or "pet") then + S.currentBook = "pet" + else + S.currentBook = "spell" + end + end + S.currentTab = 1 + S.currentPage = 1 + S.frame:Show() +end + +function SB:Hide() + if S.frame and S.frame:IsShown() then + S.frame:Hide() + end +end + +function SB:IsShown() + return S.frame and S.frame:IsShown() +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function SB:Initialize() + if S.initialized then return end + S.initialized = true + + if SFramesDB.spellBookHighestOnly == nil then SFramesDB.spellBookHighestOnly = false end + if SFramesDB.spellBookAutoReplace == nil then SFramesDB.spellBookAutoReplace = false end + + HideBlizzardSpellBook() + BuildMainFrame() + + local ef = CreateFrame("Frame", nil, UIParent) + ef:RegisterEvent("SPELLS_CHANGED") + ef:RegisterEvent("LEARNED_SPELL_IN_TAB") + ef:RegisterEvent("SPELL_UPDATE_COOLDOWN") + ef:SetScript("OnEvent", function() + if event == "LEARNED_SPELL_IN_TAB" then + AutoReplaceActionBarSpells() + end + if S.frame and S.frame:IsShown() then + FullRefresh() + end + end) +end + +-------------------------------------------------------------------------------- +-- Hook ToggleSpellBook +-------------------------------------------------------------------------------- +local origToggleSpellBook = ToggleSpellBook +ToggleSpellBook = function(bookType) + if SFramesDB and SFramesDB.enableSpellBook == false then + if origToggleSpellBook then + origToggleSpellBook(bookType) + end + return + end + SB:Toggle(bookType) +end + +-------------------------------------------------------------------------------- +-- Bootstrap +-------------------------------------------------------------------------------- +local bootstrap = CreateFrame("Frame", nil, UIParent) +bootstrap:RegisterEvent("PLAYER_LOGIN") +bootstrap:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + if SFramesDB.enableSpellBook == nil then + SFramesDB.enableSpellBook = true + end + if SFramesDB.enableSpellBook ~= false then + SB:Initialize() + end + end +end) diff --git a/StatSummary.lua b/StatSummary.lua new file mode 100644 index 0000000..122fe59 --- /dev/null +++ b/StatSummary.lua @@ -0,0 +1,868 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Stat Summary & Equipment List (StatSummary.lua) +-------------------------------------------------------------------------------- + +SFrames.StatSummary = {} +local SS = SFrames.StatSummary +local summaryFrame + +-------------------------------------------------------------------------------- +-- Theme (reuse CharacterPanel colors) +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + enchanted = { 0.30, 1, 0.30 }, + noEnchant = { 1, 0.35, 0.35 }, + setColor = { 1, 0.85, 0.0 }, + defColor = { 0.35, 0.90, 0.25 }, + physColor = { 0.90, 0.75, 0.55 }, + spellColor = { 0.60, 0.80, 1.00 }, + regenColor = { 0.40, 0.90, 0.70 }, + statColors = { + str = { 0.78, 0.61, 0.43 }, + agi = { 0.52, 1, 0.52 }, + sta = { 0.75, 0.55, 0.25 }, + int = { 0.41, 0.80, 0.94 }, + spi = { 1, 1, 1 }, + }, + resistColors = { + arcane = { 0.95, 0.90, 0.40 }, + fire = { 1, 0.50, 0.15 }, + nature = { 0.35, 0.90, 0.25 }, + frost = { 0.45, 0.70, 1.00 }, + shadow = { 0.60, 0.35, 0.90 }, + }, +}) + +local PANEL_W = 220 +local PANEL_H = 490 +local HEADER_H = 24 +local ROW_H = 14 +local SECTION_GAP = 4 +local SIDE_PAD = 6 + +local QUALITY_COLORS = { + [0] = { 0.62, 0.62, 0.62 }, + [1] = { 1, 1, 1 }, + [2] = { 0.12, 1, 0 }, + [3] = { 0.0, 0.44, 0.87 }, + [4] = { 0.64, 0.21, 0.93 }, + [5] = { 1, 0.5, 0 }, +} + +local ENCHANTABLE_SLOTS = { + { id = 1, name = "HeadSlot", label = "头部" }, + { id = 3, name = "ShoulderSlot", label = "肩部" }, + { id = 15, name = "BackSlot", label = "背部" }, + { id = 5, name = "ChestSlot", label = "胸部" }, + { id = 9, name = "WristSlot", label = "手腕" }, + { id = 10, name = "HandsSlot", label = "手套" }, + { id = 7, name = "LegsSlot", label = "腿部" }, + { id = 8, name = "FeetSlot", label = "脚部" }, + { id = 11, name = "Finger0Slot", label = "戒指1" }, + { id = 12, name = "Finger1Slot", label = "戒指2" }, + { id = 16, name = "MainHandSlot", label = "主手" }, + { id = 17, name = "SecondaryHandSlot",label = "副手" }, + { id = 18, name = "RangedSlot", label = "远程" }, +} + +local widgetId = 0 + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function NN(p) + widgetId = widgetId + 1 + return "SFramesSS" .. (p or "") .. widgetId +end + +local function SetBD(f, bg, bd) + f:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + local b = bg or T.bg + local d = bd or T.border + f:SetBackdropColor(b[1], b[2], b[3], b[4] or 1) + f:SetBackdropBorderColor(d[1], d[2], d[3], d[4] or 1) +end + +local function SetPBD(f, bg, bd) + f:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 } + }) + if bg then f:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) end + if bd then f:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) end +end + +local function FS(parent, size, jh, color) + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), size or 10, "OUTLINE") + fs:SetJustifyH(jh or "LEFT") + local c = color or T.valueText + fs:SetTextColor(c[1], c[2], c[3]) + return fs +end + +-------------------------------------------------------------------------------- +-- Stat helpers +-------------------------------------------------------------------------------- +local function GetIBL() + if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("ItemBonusLib-1.0") then + local ok, lib = pcall(function() return AceLibrary("ItemBonusLib-1.0") end) + if ok and lib then return lib end + end + return nil +end + +local function GearB(key) + local lib = GetIBL() + if lib and lib.GetBonus then return lib:GetBonus(key) or 0 end + return 0 +end + +local function TryAPI(names) + for i = 1, table.getn(names) do + local fn = _G[names[i]] + if fn then + local ok, val = pcall(fn) + if ok and type(val) == "number" and val > 0 then return val end + end + end + return 0 +end + +local function TryAPIa(names, a1) + for i = 1, table.getn(names) do + local fn = _G[names[i]] + if fn then + local ok, val = pcall(fn, a1) + if ok and type(val) == "number" and val > 0 then return val end + end + end + return 0 +end + +local function GetCS() + if SFrames and SFrames.CharacterPanel and SFrames.CharacterPanel.CS then + return SFrames.CharacterPanel.CS + end + return nil +end + +-------------------------------------------------------------------------------- +-- Scroll frame +-------------------------------------------------------------------------------- +local function MakeScroll(parent, w, h) + local holder = CreateFrame("Frame", NN("H"), parent) + holder:SetWidth(w) + holder:SetHeight(h) + + local sf = CreateFrame("ScrollFrame", NN("S"), holder) + sf:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0) + sf:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -8, 0) + + local child = CreateFrame("Frame", NN("C"), sf) + child:SetWidth(w - 12) + child:SetHeight(1) + sf:SetScrollChild(child) + + local sb = CreateFrame("Slider", NN("B"), holder) + sb:SetWidth(5) + sb:SetPoint("TOPRIGHT", holder, "TOPRIGHT", -1, -2) + sb:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -1, 2) + sb:SetOrientation("VERTICAL") + sb:SetMinMaxValues(0, 1) + sb:SetValue(0) + SetPBD(sb, { 0.1, 0.1, 0.12, 0.4 }, { 0.15, 0.15, 0.18, 0.3 }) + + local thumb = sb:CreateTexture(nil, "OVERLAY") + thumb:SetTexture("Interface\\Buttons\\WHITE8X8") + thumb:SetVertexColor(0.4, 0.4, 0.48, 0.7) + thumb:SetWidth(5) + thumb:SetHeight(24) + sb:SetThumbTexture(thumb) + + sb:SetScript("OnValueChanged", function() + sf:SetVerticalScroll(this:GetValue()) + end) + holder:EnableMouseWheel(1) + holder:SetScript("OnMouseWheel", function() + local cur = sb:GetValue() + local step = 24 + local lo, mx = sb:GetMinMaxValues() + if arg1 > 0 then + sb:SetValue(math.max(cur - step, 0)) + else + sb:SetValue(math.min(cur + step, mx)) + end + end) + + holder.sf = sf + holder.child = child + holder.sb = sb + holder.UpdateH = function(_, ch) + child:SetHeight(ch) + local visH = sf:GetHeight() + local maxS = math.max(ch - visH, 0) + sb:SetMinMaxValues(0, maxS) + if maxS == 0 then sb:Hide() else sb:Show() end + sb:SetValue(math.min(sb:GetValue(), maxS)) + end + return holder +end + +-------------------------------------------------------------------------------- +-- Enchant detection (comprehensive for vanilla 1.12 / Turtle WoW) +-- +-- Vanilla WoW tooltip green text types: +-- 1. Enchant effects (what we detect) +-- 2. "装备:" / "Equip:" item innate effects +-- 3. "使用:" / "Use:" item use effects +-- 4. "击中时可能:" / "Chance on hit:" proc effects +-- 5. "(X) 套装:" active set bonuses +-- Strategy: exclude #2-5, then positive-match enchant patterns. +-- +-- Covers: standard enchanting, libram/arcanum (head/legs), ZG class enchants, +-- ZG/Naxx shoulder augments, armor kits, weapon chains, scopes, spurs, +-- counterweights, and Turtle WoW custom enchants. +-------------------------------------------------------------------------------- +local scanTip + +local function EnsureTip() + if not scanTip then + scanTip = CreateFrame("GameTooltip", "SFramesSScanTip", nil, "GameTooltipTemplate") + end + scanTip:SetOwner(UIParent, "ANCHOR_NONE") + return scanTip +end + +local PROC_ENCHANTS = { + "十字军", "Crusader", + "吸取生命", "Lifestealing", + "灼热武器", "Fiery Weapon", "火焰武器", + "寒冰", "Icy Chill", + "邪恶武器", "Unholy Weapon", + "恶魔杀手", "Demonslaying", + "无法被缴械", "Cannot be Disarmed", +} + +local function IsEnchantLine(txt) + if string.find(txt, "%+%d") then return true end + if string.find(txt, "%d+%%") then return true end + for i = 1, table.getn(PROC_ENCHANTS) do + if string.find(txt, PROC_ENCHANTS[i]) then return true end + end + return false +end + +local function GetEnchant(slotId) + local tip = EnsureTip() + tip:ClearLines() + tip:SetInventoryItem("player", slotId) + local n = tip:NumLines() + if not n or n < 2 then return false, nil end + + for i = 2, n do + local obj = _G["SFramesSScanTipTextLeft" .. i] + if obj then + local txt = obj:GetText() + if txt and txt ~= "" then + local r, g, b = obj:GetTextColor() + if g > 0.8 and r < 0.5 and b < 0.5 then + local skip = false + if string.find(txt, "装备:") or string.find(txt, "装备:") or string.find(txt, "Equip:") then + skip = true + elseif string.find(txt, "使用:") or string.find(txt, "使用:") or string.find(txt, "Use:") then + skip = true + elseif string.find(txt, "击中时可能") or string.find(txt, "Chance on hit") then + skip = true + elseif string.find(txt, "^%(") then + skip = true + elseif string.find(txt, "套装:") or string.find(txt, "套装:") or string.find(txt, "Set:") then + skip = true + end + if not skip and IsEnchantLine(txt) then + return true, txt + end + end + end + end + end + return false, nil +end + +-------------------------------------------------------------------------------- +-- Set bonus detection +-------------------------------------------------------------------------------- +local function GetSets() + local tip = EnsureTip() + local sets = {} + local seen = {} + local slots = { 1,2,3,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19 } + + for si = 1, table.getn(slots) do + local sid = slots[si] + local link = GetInventoryItemLink("player", sid) + if link then + tip:ClearLines() + tip:SetOwner(UIParent, "ANCHOR_NONE") + tip:SetInventoryItem("player", sid) + local nl = tip:NumLines() + if nl then + for li = 1, nl do + local obj = _G["SFramesSScanTipTextLeft" .. li] + if obj then + local txt = obj:GetText() + if txt then + local a, b, sn, sc, sm + a, b, sn, sc, sm = string.find(txt, "^(.-)%s*%((%d+)/(%d+)%)$") + if sn and sc and sm then + sn = string.gsub(sn, "^%s+", "") + sn = string.gsub(sn, "%s+$", "") + if sn ~= "" and not seen[sn] then + seen[sn] = true + table.insert(sets, { + name = sn, + current = tonumber(sc) or 0, + max = tonumber(sm) or 0, + }) + end + end + end + end + end + end + end + end + return sets +end + +-------------------------------------------------------------------------------- +-- Build the panel +-------------------------------------------------------------------------------- +local function BuildPanel() + if summaryFrame then return summaryFrame end + + local f = CreateFrame("Frame", "SFramesStatSummary", UIParent) + f:SetWidth(PANEL_W) + f:SetHeight(PANEL_H) + f:SetPoint("CENTER", UIParent, "CENTER", 200, 0) + f:SetFrameStrata("DIALOG") + f:SetFrameLevel(100) + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + f:SetClampedToScreen(true) + SetBD(f, T.bg, T.border) + f:Hide() + + -- Header + local hdr = FS(f, 11, "LEFT", T.gold) + hdr:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD + 2, -6) + hdr:SetText("属性总览") + + -- Tab buttons + local tabW = 60 + f.tabs = {} + f.curTab = 1 + local tNames = { "属性", "装备" } + for i = 1, 2 do + local btn = CreateFrame("Button", NN("T"), f) + btn:SetWidth(tabW) + btn:SetHeight(16) + btn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -(SIDE_PAD + (2 - i) * (tabW + 2) + 20), -5) + btn:SetFrameLevel(f:GetFrameLevel() + 2) + SetPBD(btn, T.tabBg, T.tabBorder) + local lbl = FS(btn, 9, "CENTER", T.tabText) + lbl:SetPoint("CENTER", btn, "CENTER", 0, 0) + lbl:SetText(tNames[i]) + btn.lbl = lbl + btn.idx = i + btn:SetScript("OnClick", function() SS:SetTab(this.idx) end) + f.tabs[i] = btn + end + + -- Close + local cb = CreateFrame("Button", nil, f) + cb:SetWidth(14) + cb:SetHeight(14) + cb:SetPoint("TOPRIGHT", f, "TOPRIGHT", -5, -5) + cb:SetFrameLevel(f:GetFrameLevel() + 3) + SetBD(cb, T.buttonDownBg or { 0.35, 0.06, 0.06, 0.85 }, T.btnBorder or { 0.45, 0.1, 0.1, 0.6 }) + local cx = FS(cb, 8, "CENTER", { 1, 0.7, 0.7 }) + cx:SetPoint("CENTER", cb, "CENTER", 0, 0) + cx:SetText("x") + cb:SetScript("OnClick", function() f:Hide() end) + + -- Sep + local sep = f:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + sep:SetHeight(1) + sep:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -HEADER_H) + sep:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -HEADER_H) + + -- Content + local cH = PANEL_H - HEADER_H - 8 + local cW = PANEL_W - 8 + + f.statsScroll = MakeScroll(f, cW, cH) + f.statsScroll:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -(HEADER_H + 2)) + + f.equipScroll = MakeScroll(f, cW, cH) + f.equipScroll:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -(HEADER_H + 2)) + f.equipScroll:Hide() + + summaryFrame = f + return f +end + +-------------------------------------------------------------------------------- +-- Tab switching +-------------------------------------------------------------------------------- +function SS:SetTab(idx) + if not summaryFrame then return end + summaryFrame.curTab = idx + for i = 1, 2 do + local btn = summaryFrame.tabs[i] + if i == idx then + SetPBD(btn, T.tabActiveBg, T.tabActiveBorder) + btn.lbl:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) + else + SetPBD(btn, T.tabBg, T.tabBorder) + btn.lbl:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) + end + end + if idx == 1 then + summaryFrame.statsScroll:Show() + summaryFrame.equipScroll:Hide() + local ok, err = pcall(function() SS:BuildStats() end) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444SS BuildStats err: " .. tostring(err) .. "|r") + end + else + summaryFrame.statsScroll:Hide() + summaryFrame.equipScroll:Show() + local ok, err = pcall(function() SS:BuildEquip() end) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444SS BuildEquip err: " .. tostring(err) .. "|r") + end + end +end + +-------------------------------------------------------------------------------- +-- Helpers for building rows +-------------------------------------------------------------------------------- +local function HideRows(parent) + if parent._r then + for i = 1, table.getn(parent._r) do + local r = parent._r[i] + if r and r.Hide then r:Hide() end + end + end + parent._r = {} +end + +local function AddHeader(p, txt, y, clr) + local f1 = FS(p, 10, "LEFT", clr or T.sectionTitle) + f1:SetPoint("TOPLEFT", p, "TOPLEFT", 4, y) + f1:SetText(txt) + tinsert(p._r, f1) + + local s = p:CreateTexture(nil, "ARTWORK") + s:SetTexture("Interface\\Buttons\\WHITE8X8") + s:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + s:SetHeight(1) + s:SetPoint("TOPLEFT", p, "TOPLEFT", 4, y - 13) + s:SetPoint("TOPRIGHT", p, "TOPRIGHT", -4, y - 13) + tinsert(p._r, s) + return y - 16 +end + +local function AddRow(p, lbl, val, y, lc, vc) + local f1 = FS(p, 9, "LEFT", lc or T.labelText) + f1:SetPoint("TOPLEFT", p, "TOPLEFT", 8, y) + f1:SetText(lbl) + tinsert(p._r, f1) + + local f2 = FS(p, 9, "RIGHT", vc or T.valueText) + f2:SetPoint("TOPRIGHT", p, "TOPRIGHT", -12, y) + f2:SetWidth(100) + f2:SetJustifyH("RIGHT") + f2:SetText(val) + tinsert(p._r, f2) + return y - ROW_H +end + +-------------------------------------------------------------------------------- +-- Stats page +-------------------------------------------------------------------------------- +function SS:BuildStats() + local child = summaryFrame.statsScroll.child + HideRows(child) + local y = -4 + + -- Primary Stats + y = AddHeader(child, "主属性:", y) + local sn = { "力量", "敏捷", "耐力", "智力", "精神" } + local sc = { T.statColors.str, T.statColors.agi, T.statColors.sta, T.statColors.int, T.statColors.spi } + for i = 1, 5 do + local _, eff = UnitStat("player", i) + y = AddRow(child, sn[i], tostring(math.floor(eff or 0)), y, sc[i], sc[i]) + end + + y = y - SECTION_GAP + + -- Defense + y = AddHeader(child, "防御属性:", y, T.defColor) + local hp = UnitHealthMax("player") or 0 + y = AddRow(child, "生命值", tostring(hp), y, nil, T.defColor) + + local dodge = 0 + if GetDodgeChance then dodge = GetDodgeChance() or 0 end + if dodge == 0 then dodge = GearB("DODGE") end + y = AddRow(child, "躲闪", string.format("%.2f%%", dodge), y) + + local baseA, effA = UnitArmor("player") + effA = math.floor(effA or baseA or 0) + y = AddRow(child, "护甲", tostring(effA), y) + + local lvl = UnitLevel("player") or 60 + local k1 = 400 + 85 * lvl + local p1 = effA / (effA + k1) * 100 + if p1 > 75 then p1 = 75 end + y = AddRow(child, "物理免伤同级", string.format("%.2f%%", p1), y) + + local k2 = 400 + 85 * (lvl + 3) + local p2 = effA / (effA + k2) * 100 + if p2 > 75 then p2 = 75 end + y = AddRow(child, "物理免伤骷髅级", string.format("%.2f%%", p2), y) + + y = y - SECTION_GAP + + -- Resistances + y = AddHeader(child, "抗性:", y, T.resistColors.arcane) + local rInfo = { + { 6, "奥术抗性", T.resistColors.arcane }, + { 2, "火焰抗性", T.resistColors.fire }, + { 3, "自然抗性", T.resistColors.nature }, + { 4, "冰霜抗性", T.resistColors.frost }, + { 5, "暗影抗性", T.resistColors.shadow }, + } + for i = 1, table.getn(rInfo) do + local ri = rInfo[i] + local _, tot = UnitResistance("player", ri[1]) + y = AddRow(child, ri[2], tostring(math.floor(tot or 0)), y, ri[3], ri[3]) + end + + y = y - SECTION_GAP + + -- Physical + y = AddHeader(child, "物理:", y, T.physColor) + local mb, mp, mn = UnitAttackPower("player") + local mAP = math.floor((mb or 0) + (mp or 0) + (mn or 0)) + local rb, rp, rn = UnitRangedAttackPower("player") + local rAP = math.floor((rb or 0) + (rp or 0) + (rn or 0)) + y = AddRow(child, "近战/远程攻强", mAP .. "/" .. rAP, y, nil, T.physColor) + + local cs = GetCS() + local mHit = cs and cs.SafeGetMeleeHit and cs.SafeGetMeleeHit() or 0 + if mHit == 0 then + mHit = TryAPI({ "GetHitModifier", "GetMeleeHitModifier" }) + if mHit == 0 then mHit = GearB("TOHIT") end + end + y = AddRow(child, "近战/远程命中", string.format("+%d%%/+%d%%", mHit, mHit), y) + + local mCrit = cs and cs.SafeGetMeleeCrit and cs.SafeGetMeleeCrit() or 0 + if mCrit == 0 then + mCrit = TryAPI({ "GetCritChance", "GetMeleeCritChance" }) + if mCrit == 0 then mCrit = GearB("CRIT") end + end + local rCrit = cs and cs.SafeGetRangedCrit and cs.SafeGetRangedCrit() or 0 + if rCrit == 0 then + rCrit = TryAPI({ "GetRangedCritChance" }) + if rCrit == 0 then rCrit = mCrit end + end + y = AddRow(child, "近战/远程暴击", string.format("%.2f%%/%.2f%%", mCrit, rCrit), y) + + y = y - SECTION_GAP + + -- Spell + y = AddHeader(child, "法术:", y, T.spellColor) + local mana = UnitManaMax("player") or 0 + y = AddRow(child, "法力值", tostring(mana), y, nil, T.spellColor) + + local sDmg = 0 + if GetSpellBonusDamage then + for s = 2, 7 do + local d = GetSpellBonusDamage(s) or 0 + if d > sDmg then sDmg = d end + end + end + if sDmg == 0 then + local lib = GetIBL() + if lib and lib.GetBonus then + local baseDmg = lib:GetBonus("DMG") or 0 + sDmg = baseDmg + local schools = { "FIREDMG","FROSTDMG","SHADOWDMG","ARCANEDMG","NATUREDMG","HOLYDMG" } + for _, sk in ipairs(schools) do + local sv = baseDmg + (lib:GetBonus(sk) or 0) + if sv > sDmg then sDmg = sv end + end + end + end + y = AddRow(child, "法术伤害", tostring(math.floor(sDmg)), y) + + local sHeal = 0 + if GetSpellBonusHealing then sHeal = GetSpellBonusHealing() or 0 end + if sHeal == 0 then sHeal = GearB("HEAL") end + y = AddRow(child, "法术治疗", tostring(math.floor(sHeal)), y) + + local sCrit = cs and cs.SafeGetSpellCrit and cs.SafeGetSpellCrit() or 0 + if sCrit == 0 then + sCrit = TryAPIa({ "GetSpellCritChance" }, 2) + if sCrit == 0 then sCrit = GearB("SPELLCRIT") end + end + y = AddRow(child, "法术暴击", string.format("%.2f%%", sCrit), y) + + local sHit = cs and cs.SafeGetSpellHit and cs.SafeGetSpellHit() or 0 + if sHit == 0 then + sHit = TryAPI({ "GetSpellHitModifier" }) + if sHit == 0 then sHit = GearB("SPELLTOHIT") end + end + y = AddRow(child, "法术命中", string.format("+%d%%", sHit), y) + + local sHaste = GearB("SPELLHASTE") + if sHaste > 0 then + y = AddRow(child, "急速", string.format("+%.2f%%", sHaste), y) + end + + local sPen = GearB("SPELLPEN") + if sPen > 0 then + y = AddRow(child, "法术穿透", "+" .. tostring(math.floor(sPen)), y) + end + + y = y - SECTION_GAP + + -- Regen + y = AddHeader(child, "回复:", y, T.regenColor) + + local mp5 = GearB("MANAREG") + y = AddRow(child, "装备回蓝", tostring(math.floor(mp5)) .. " MP/5s", y, nil, T.regenColor) + + local _, spi = UnitStat("player", 5) + spi = math.floor(spi or 0) + local spReg = math.floor(15 + spi / 5) + y = AddRow(child, "精神回蓝", spReg .. " MP/2s", y) + + y = y - SECTION_GAP + + -- Set bonuses + local ok2, sets = pcall(GetSets) + if ok2 and sets and table.getn(sets) > 0 then + y = AddHeader(child, "套装:", y, T.setColor) + for i = 1, table.getn(sets) do + local s = sets[i] + y = AddRow(child, s.name, "(" .. s.current .. "/" .. s.max .. ")", y, T.setColor, T.setColor) + end + end + + summaryFrame.statsScroll:UpdateH(math.abs(y) + 12) +end + +-------------------------------------------------------------------------------- +-- Equipment page +-------------------------------------------------------------------------------- +function SS:BuildEquip() + local child = summaryFrame.equipScroll.child + HideRows(child) + local y = -4 + + y = AddHeader(child, "装备列表 & 附魔检查:", y, T.gold) + + local totalSlots = 0 + local enchCount = 0 + + for si = 1, table.getn(ENCHANTABLE_SLOTS) do + local slot = ENCHANTABLE_SLOTS[si] + local link = GetInventoryItemLink("player", slot.id) + + if link then + totalSlots = totalSlots + 1 + local _, _, rawName = string.find(link, "%[(.-)%]") + local itemName = rawName or slot.label + + local quality = nil + local _, _, qH = string.find(link, "|c(%x+)|H") + local qMap = {} + qMap["ff9d9d9d"] = 0 + qMap["ffffffff"] = 1 + qMap["ff1eff00"] = 2 + qMap["ff0070dd"] = 3 + qMap["ffa335ee"] = 4 + qMap["ffff8000"] = 5 + if qH then quality = qMap[qH] end + local nc = (quality and QUALITY_COLORS[quality]) or T.valueText + + -- Slot label + local sf = FS(child, 8, "LEFT", T.dimText) + sf:SetPoint("TOPLEFT", child, "TOPLEFT", 4, y) + sf:SetText(slot.label) + sf:SetWidth(32) + tinsert(child._r, sf) + + -- Item name + local nf = FS(child, 9, "LEFT", nc) + nf:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y) + nf:SetWidth(130) + if string.len(itemName) > 18 then + itemName = string.sub(itemName, 1, 16) .. ".." + end + nf:SetText(itemName) + tinsert(child._r, nf) + + -- Enchant check + local hasE, eTxt = false, nil + local eOk, eR1, eR2 = pcall(GetEnchant, slot.id) + if eOk then hasE = eR1; eTxt = eR2 end + + local ico = FS(child, 9, "RIGHT") + ico:SetPoint("TOPRIGHT", child, "TOPRIGHT", -8, y) + if hasE then + ico:SetTextColor(T.enchanted[1], T.enchanted[2], T.enchanted[3]) + ico:SetText("*") + enchCount = enchCount + 1 + else + ico:SetTextColor(T.noEnchant[1], T.noEnchant[2], T.noEnchant[3]) + ico:SetText("-") + end + tinsert(child._r, ico) + + y = y - ROW_H + + if hasE and eTxt then + local ef = FS(child, 8, "LEFT", T.enchanted) + ef:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y) + ef:SetWidth(155) + ef:SetText(" " .. eTxt) + tinsert(child._r, ef) + y = y - 12 + elseif not hasE then + local ef = FS(child, 8, "LEFT", T.noEnchant) + ef:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y) + ef:SetText(" 未附魔") + tinsert(child._r, ef) + y = y - 12 + end + y = y - 2 + else + local sf = FS(child, 8, "LEFT", T.dimText) + sf:SetPoint("TOPLEFT", child, "TOPLEFT", 4, y) + sf:SetText(slot.label) + tinsert(child._r, sf) + + local ef = FS(child, 9, "LEFT", T.dimText) + ef:SetPoint("TOPLEFT", child, "TOPLEFT", 38, y) + ef:SetText("-- 未装备 --") + tinsert(child._r, ef) + y = y - ROW_H - 2 + end + end + + y = y - SECTION_GAP + y = AddHeader(child, "附魔统计:", y, T.gold) + local sc2 = (enchCount == totalSlots) and T.enchanted or T.noEnchant + y = AddRow(child, "已附魔/总装备", enchCount .. "/" .. totalSlots, y, nil, sc2) + if enchCount < totalSlots then + y = AddRow(child, "缺少附魔", tostring(totalSlots - enchCount) .. " 件", y, T.noEnchant, T.noEnchant) + end + + summaryFrame.equipScroll:UpdateH(math.abs(y) + 12) +end + +-------------------------------------------------------------------------------- +-- Public API +-------------------------------------------------------------------------------- +function SS:Toggle() + local ok, err = pcall(function() + BuildPanel() + if summaryFrame:IsShown() then + summaryFrame:Hide() + return + end + local cpf = _G["SFramesCharacterPanel"] + if cpf and cpf:IsShown() then + summaryFrame:ClearAllPoints() + summaryFrame:SetPoint("TOPLEFT", cpf, "TOPRIGHT", 2, 0) + else + summaryFrame:ClearAllPoints() + summaryFrame:SetPoint("CENTER", UIParent, "CENTER", 200, 0) + end + local scale = SFramesDB and SFramesDB.charPanelScale or 1.0 + summaryFrame:SetScale(scale) + summaryFrame:Show() + SS:SetTab(summaryFrame.curTab or 1) + end) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] SS:Toggle error: " .. tostring(err) .. "|r") + end +end + +function SS:Show() + local ok, err = pcall(function() + BuildPanel() + local cpf = _G["SFramesCharacterPanel"] + if cpf and cpf:IsShown() then + summaryFrame:ClearAllPoints() + summaryFrame:SetPoint("TOPLEFT", cpf, "TOPRIGHT", 2, 0) + end + local scale = SFramesDB and SFramesDB.charPanelScale or 1.0 + summaryFrame:SetScale(scale) + summaryFrame:Show() + SS:SetTab(summaryFrame.curTab or 1) + end) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] SS:Show error: " .. tostring(err) .. "|r") + end +end + +function SS:Hide() + if summaryFrame then summaryFrame:Hide() end +end + +function SS:IsShown() + return summaryFrame and summaryFrame:IsShown() +end + +function SS:Refresh() + if not summaryFrame or not summaryFrame:IsShown() then return end + SS:SetTab(summaryFrame.curTab or 1) +end + +-------------------------------------------------------------------------------- +-- Events +-------------------------------------------------------------------------------- +local evf = CreateFrame("Frame", "SFramesSSEv", UIParent) +evf:RegisterEvent("UNIT_INVENTORY_CHANGED") +evf:RegisterEvent("PLAYER_AURAS_CHANGED") +evf:RegisterEvent("UNIT_ATTACK_POWER") +evf:RegisterEvent("UNIT_RESISTANCES") +evf:SetScript("OnEvent", function() + if summaryFrame and summaryFrame:IsShown() then + SS:Refresh() + end +end) + +DEFAULT_CHAT_FRAME:AddMessage("SF: Loading StatSummary.lua...") diff --git a/Theme.lua b/Theme.lua new file mode 100644 index 0000000..0b760df --- /dev/null +++ b/Theme.lua @@ -0,0 +1,269 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Central Theme Engine (Theme.lua) +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.Theme = {} +SFrames.ActiveTheme = {} + +local function HSVtoRGB(h, s, v) + if s <= 0 then return v, v, v end + h = h - math.floor(h / 360) * 360 + local hh = h / 60 + local i = math.floor(hh) + local f = hh - i + local p = v * (1 - s) + local q = v * (1 - s * f) + local t = v * (1 - s * (1 - f)) + if i == 0 then return v, t, p + elseif i == 1 then return q, v, p + elseif i == 2 then return p, v, t + elseif i == 3 then return p, q, v + elseif i == 4 then return t, p, v + else return v, p, q end +end + +local function toHexChar(n) + if n < 10 then return string.char(48 + n) end + return string.char(97 + n - 10) +end + +local function RGBtoHex(r, g, b) + local rr = math.floor(r * 255 + 0.5) + local gg = math.floor(g * 255 + 0.5) + local bb = math.floor(b * 255 + 0.5) + return "ff" + .. toHexChar(math.floor(rr / 16)) .. toHexChar(rr - math.floor(rr / 16) * 16) + .. toHexChar(math.floor(gg / 16)) .. toHexChar(gg - math.floor(gg / 16) * 16) + .. toHexChar(math.floor(bb / 16)) .. toHexChar(bb - math.floor(bb / 16) * 16) +end + +SFrames.Theme.Presets = {} +SFrames.Theme.Presets["pink"] = { name = "Pink", hue = 330, satMul = 1.00 } +SFrames.Theme.Presets["frost"] = { name = "Frost", hue = 210, satMul = 1.00 } +SFrames.Theme.Presets["emerald"] = { name = "Emerald", hue = 140, satMul = 0.85 } +SFrames.Theme.Presets["flame"] = { name = "Flame", hue = 25, satMul = 0.90 } +SFrames.Theme.Presets["shadow"] = { name = "Shadow", hue = 270, satMul = 0.90 } +SFrames.Theme.Presets["golden"] = { name = "Golden", hue = 45, satMul = 0.80 } +SFrames.Theme.Presets["teal"] = { name = "Teal", hue = 175, satMul = 0.85 } +SFrames.Theme.Presets["crimson"] = { name = "Crimson", hue = 355, satMul = 0.90 } +SFrames.Theme.Presets["holy"] = { name = "Holy", hue = 220, satMul = 0.15 } + +SFrames.Theme.PresetOrder = { "pink", "frost", "emerald", "flame", "shadow", "golden", "teal", "crimson", "holy" } + +SFrames.Theme.ClassMap = {} +SFrames.Theme.ClassMap["WARRIOR"] = "crimson" +SFrames.Theme.ClassMap["MAGE"] = "frost" +SFrames.Theme.ClassMap["ROGUE"] = "teal" +SFrames.Theme.ClassMap["DRUID"] = "flame" +SFrames.Theme.ClassMap["HUNTER"] = "emerald" +SFrames.Theme.ClassMap["SHAMAN"] = "frost" +SFrames.Theme.ClassMap["PRIEST"] = "holy" +SFrames.Theme.ClassMap["WARLOCK"] = "shadow" +SFrames.Theme.ClassMap["PALADIN"] = "golden" + +local function GenerateTheme(H, satMul) + satMul = satMul or 1.0 + local function S(s) + local v = s * satMul + if v > 1 then v = 1 end + return v + end + local function C3(s, v) + local r, g, b = HSVtoRGB(H, S(s), v) + return { r, g, b } + end + local function C4(s, v, a) + local r, g, b = HSVtoRGB(H, S(s), v) + return { r, g, b, a } + end + local t = {} + t.accent = C4(0.40, 0.80, 0.98) + t.accentDark = C3(0.45, 0.55) + t.accentLight = C3(0.30, 1.00) + t.accentHex = RGBtoHex(t.accentLight[1], t.accentLight[2], t.accentLight[3]) + t.panelBg = C4(0.50, 0.12, 0.95) + t.panelBorder = C4(0.45, 0.55, 0.90) + t.headerBg = C4(0.60, 0.10, 0.98) + t.sectionBg = C4(0.43, 0.14, 0.82) + t.sectionBorder = C4(0.38, 0.45, 0.86) + t.bg = t.panelBg + t.border = t.panelBorder + t.slotBg = C4(0.20, 0.07, 0.90) + t.slotBorder = C4(0.10, 0.28, 0.80) + t.slotHover = C4(0.38, 0.40, 0.90) + t.slotSelected = C4(0.43, 0.70, 1.00) + t.buttonBg = C4(0.44, 0.18, 0.94) + t.buttonBorder = C4(0.40, 0.50, 0.90) + t.buttonHoverBg = C4(0.47, 0.30, 0.96) + t.buttonDownBg = C4(0.50, 0.14, 0.96) + t.buttonDisabledBg = C4(0.43, 0.14, 0.65) + t.buttonActiveBg = C4(0.52, 0.42, 0.98) + t.buttonActiveBorder = C4(0.42, 0.90, 1.00) + t.buttonText = C3(0.16, 0.90) + t.buttonActiveText = C3(0.08, 1.00) + t.buttonDisabledText = C4(0.14, 0.55, 0.68) + t.btnBg = t.buttonBg + t.btnBorder = t.buttonBorder + t.btnHoverBg = t.buttonHoverBg + t.btnHoverBd = C4(0.40, 0.80, 0.98) + t.btnDownBg = t.buttonDownBg + t.btnText = t.buttonText + t.btnActiveText = t.buttonActiveText + t.btnDisabledText = C3(0.14, 0.40) + t.btnHover = C4(0.47, 0.30, 0.95) + t.btnHoverBorder = t.btnHoverBd + t.tabBg = t.buttonBg + t.tabBorder = t.buttonBorder + t.tabActiveBg = C4(0.50, 0.32, 0.96) + t.tabActiveBorder = C4(0.40, 0.80, 0.98) + t.tabText = C3(0.21, 0.70) + t.tabActiveText = t.buttonActiveText + t.checkBg = t.buttonBg + t.checkBorder = t.buttonBorder + t.checkHoverBorder = C4(0.40, 0.80, 0.95) + t.checkFill = C4(0.43, 0.88, 0.98) + t.checkOn = C3(0.40, 0.80) + t.checkOff = C4(0.40, 0.25, 0.60) + t.sliderTrack = C4(0.45, 0.22, 0.90) + t.sliderFill = C4(0.35, 0.85, 0.92) + t.sliderThumb = C4(0.25, 1.00, 0.95) + t.text = C3(0.11, 0.92) + t.title = C3(0.30, 1.00) + t.gold = t.title + t.nameText = C3(0.06, 0.92) + t.dimText = C3(0.25, 0.60) + t.bodyText = C3(0.05, 0.82) + t.sectionTitle = C3(0.24, 0.90) + t.catHeader = C3(0.31, 0.80) + t.colHeader = C3(0.25, 0.80) + t.labelText = C3(0.23, 0.65) + t.valueText = t.text + t.subText = t.labelText + t.pageText = C3(0.19, 0.80) + t.objectiveText = C3(0.10, 0.90) + t.optionText = t.tabText + t.countText = t.tabText + t.trackText = C3(0.25, 0.80) + t.divider = C4(0.45, 0.55, 0.40) + t.sepColor = C4(0.44, 0.45, 0.50) + t.scrollThumb = C4(0.45, 0.55, 0.70) + t.scrollTrack = C4(0.50, 0.08, 0.50) + t.inputBg = C4(0.50, 0.08, 0.95) + t.inputBorder = C4(0.38, 0.40, 0.80) + t.searchBg = C4(0.50, 0.08, 0.80) + t.searchBorder = C4(0.38, 0.40, 0.60) + t.progressBg = C4(0.50, 0.08, 0.90) + t.progressFill = C4(0.50, 0.70, 1.00) + t.modelBg = C4(0.60, 0.08, 0.85) + t.modelBorder = C4(0.43, 0.35, 0.70) + t.emptySlot = C4(0.40, 0.25, 0.40) + t.emptySlotBg = C4(0.50, 0.08, 0.40) + t.emptySlotBd = C4(0.40, 0.25, 0.30) + t.barBg = C4(0.60, 0.10, 1.00) + t.rowNormal = C4(0.50, 0.06, 0.30) + t.rowNormalBd = C4(0.22, 0.20, 0.30) + t.raidGroup = t.sectionBg + t.raidGroupBorder = C4(0.38, 0.40, 0.70) + t.raidSlotEmpty = C4(0.50, 0.08, 0.60) + t.questSelected = C4(0.70, 0.60, 0.85) + t.questSelBorder = C4(0.47, 0.95, 1.00) + t.questSelBar = C4(0.45, 1.00, 1.00) + t.questHover = C4(0.52, 0.25, 0.50) + t.zoneHeader = t.catHeader + t.zoneBg = C4(0.50, 0.14, 0.50) + t.collapseIcon = C3(0.31, 0.70) + t.trackBar = C4(0.53, 0.95, 1.00) + t.trackGlow = C4(0.53, 0.95, 0.22) + t.rewardBg = C4(0.50, 0.10, 0.85) + t.rewardBorder = C4(0.45, 0.40, 0.70) + t.listBg = C4(0.50, 0.08, 0.80) + t.listBorder = C4(0.43, 0.35, 0.60) + t.detailBg = C4(0.50, 0.09, 0.92) + t.detailBorder = t.listBorder + t.selectedRowBg = C4(0.65, 0.35, 0.60) + t.selectedRowBorder = C4(0.50, 0.90, 0.70) + t.selectedNameText = { 1, 0.95, 1 } + t.overlayBg = C4(0.75, 0.04, 0.55) + t.accentLine = C4(0.50, 1.00, 0.90) + t.titleColor = t.title + t.nameColor = { 1, 1, 1 } + t.valueColor = t.text + t.labelColor = C3(0.28, 0.58) + t.dimColor = C3(0.29, 0.48) + t.clockColor = C3(0.18, 1.00) + t.timerColor = C3(0.27, 0.75) + t.brandColor = C4(0.37, 0.60, 0.70) + t.particleColor = C3(0.40, 1.00) + t.wbGold = { 1, 0.88, 0.55 } + t.wbBorder = { 0.95, 0.75, 0.25 } + t.passive = { 0.60, 0.60, 0.65 } + return t +end + +function SFrames.Theme.Extend(self, extras) + local t = {} + local src = SFrames.ActiveTheme + if src then + for k, v in pairs(src) do + t[k] = v + end + end + if extras then + for k, v in pairs(extras) do + t[k] = v + end + end + return t +end + +function SFrames.Theme.Apply(self, presetKey) + local preset = self.Presets[presetKey or "pink"] + if not preset then preset = self.Presets["pink"] end + local newTheme = GenerateTheme(preset.hue, preset.satMul) + local oldKeys = {} + for k, v in pairs(SFrames.ActiveTheme) do + table.insert(oldKeys, k) + end + for i = 1, table.getn(oldKeys) do + SFrames.ActiveTheme[oldKeys[i]] = nil + end + for k, v in pairs(newTheme) do + SFrames.ActiveTheme[k] = v + end + if SFrames.Config and SFrames.Config.colors then + local a = SFrames.ActiveTheme + SFrames.Config.colors.border = { r = a.accent[1], g = a.accent[2], b = a.accent[3], a = 1 } + SFrames.Config.colors.backdrop = { r = a.panelBg[1], g = a.panelBg[2], b = a.panelBg[3], a = a.panelBg[4] or 0.8 } + end +end + +function SFrames.Theme.GetCurrentPreset(self) + if SFramesDB and SFramesDB.Theme then + if SFramesDB.Theme.useClassTheme then + local _, class = UnitClass("player") + if class and self.ClassMap[class] then + return self.ClassMap[class] + end + end + if SFramesDB.Theme.preset and self.Presets[SFramesDB.Theme.preset] then + return SFramesDB.Theme.preset + end + end + return "pink" +end + +function SFrames.Theme.GetAccentHex(self) + return SFrames.ActiveTheme.accentHex or "ffffb3d9" +end + +SFrames.Theme.HSVtoRGB = HSVtoRGB +SFrames.Theme.RGBtoHex = RGBtoHex + +SFrames.Theme:Apply(SFrames.Theme:GetCurrentPreset()) + +local themeInitFrame = CreateFrame("Frame") +themeInitFrame:RegisterEvent("PLAYER_LOGIN") +themeInitFrame:SetScript("OnEvent", function() + SFrames.Theme:Apply(SFrames.Theme:GetCurrentPreset()) +end) diff --git a/Tooltip.lua b/Tooltip.lua new file mode 100644 index 0000000..23eab43 --- /dev/null +++ b/Tooltip.lua @@ -0,0 +1,1181 @@ +SFrames.FloatingTooltip = {} + +-------------------------------------------------------------------------------- +-- Class colors for tooltip unit-name coloring +-------------------------------------------------------------------------------- +local TT_CLASS_COLORS = { + ["WARRIOR"] = { 0.78, 0.61, 0.43 }, + ["MAGE"] = { 0.41, 0.80, 0.94 }, + ["ROGUE"] = { 1.00, 0.96, 0.41 }, + ["DRUID"] = { 1.00, 0.49, 0.04 }, + ["HUNTER"] = { 0.67, 0.83, 0.45 }, + ["SHAMAN"] = { 0.14, 0.35, 1.00 }, + ["PRIEST"] = { 1.00, 1.00, 1.00 }, + ["WARLOCK"] = { 0.58, 0.51, 0.79 }, + ["PALADIN"] = { 0.96, 0.55, 0.73 }, +} + +local TT_CLASS_REVERSE = {} +local ttClassReverseDone = false + +local function TT_BuildClassReverse() + if ttClassReverseDone then return end + ttClassReverseDone = true + if LOCALIZED_CLASS_NAMES_MALE then + for en, loc in pairs(LOCALIZED_CLASS_NAMES_MALE) do + TT_CLASS_REVERSE[loc] = en + end + end + if LOCALIZED_CLASS_NAMES_FEMALE then + for en, loc in pairs(LOCALIZED_CLASS_NAMES_FEMALE) do + TT_CLASS_REVERSE[loc] = en + end + end + local zh = { + ["战士"]="WARRIOR", ["法师"]="MAGE", ["盗贼"]="ROGUE", + ["德鲁伊"]="DRUID", ["猎人"]="HUNTER", ["萨满祭司"]="SHAMAN", + ["牧师"]="PRIEST", ["术士"]="WARLOCK", ["圣骑士"]="PALADIN", + } + for loc, en in pairs(zh) do + if not TT_CLASS_REVERSE[loc] then TT_CLASS_REVERSE[loc] = en end + end + for en, _ in pairs(TT_CLASS_COLORS) do + TT_CLASS_REVERSE[en] = en + TT_CLASS_REVERSE[string.lower(en)] = en + end +end + +local function TT_GetClassToken(unit) + if not UnitExists(unit) then return nil end + local className, classEN = UnitClass(unit) + if classEN and classEN ~= "" then return string.upper(classEN) end + if className then + TT_BuildClassReverse() + return TT_CLASS_REVERSE[className] + end + return nil +end + +local function TT_GetClassColor(classToken) + if not classToken then return nil, nil, nil end + if CUSTOM_CLASS_COLORS and CUSTOM_CLASS_COLORS[classToken] then + local c = CUSTOM_CLASS_COLORS[classToken] + return c.r, c.g, c.b + end + if RAID_CLASS_COLORS and RAID_CLASS_COLORS[classToken] then + local c = RAID_CLASS_COLORS[classToken] + return c.r, c.g, c.b + end + local c = TT_CLASS_COLORS[classToken] + if c then return c[1], c[2], c[3] end + return nil, nil, nil +end + +local function TT_ClassHex(classToken) + local r, g, b = TT_GetClassColor(classToken) + if not r then return "|cffffffff" end + return string.format("|cff%02x%02x%02x", r * 255, g * 255, b * 255) +end + +-------------------------------------------------------------------------------- +-- Backdrop helper (applied once, not every frame) +-------------------------------------------------------------------------------- +local TT_BACKDROP = { + bgFile = "Interface\\Buttons\\WHITE8X8", + insets = { left = 0, right = 0, top = 0, bottom = 0 }, +} + +local function TT_ApplyBackdrop(frame) + frame:SetBackdrop(TT_BACKDROP) + local _A = SFrames.ActiveTheme + if _A and _A.panelBg then + frame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], 0.95) + else + frame:SetBackdropColor(0.08, 0.08, 0.08, 0.95) + end +end + +-------------------------------------------------------------------------------- +-- Level difficulty color (fallback if GetDifficultyColor is unavailable) +-------------------------------------------------------------------------------- +local function TT_DifficultyColor(unitLevel) + local playerLevel = UnitLevel("player") or 1 + if unitLevel < 0 then return 1, 0, 0 end + local diff = unitLevel - playerLevel + if diff >= 5 then + return 1, 0.1, 0.1 + elseif diff >= 3 then + return 1, 0.5, 0.25 + elseif diff >= -2 then + return 1, 1, 0 + elseif diff >= -8 then + return 0.25, 0.75, 0.25 + else + return 0.5, 0.5, 0.5 + end +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function SFrames.FloatingTooltip:Initialize() + TT_ApplyBackdrop(GameTooltip) + + -------------------------------------------------------------------------- + -- Standalone backdrop frame: a SIBLING of GameTooltip (parented to + -- UIParent, not GameTooltip) at the same TOOLTIP strata but one frame- + -- level below. This guarantees it renders directly behind the tooltip + -- text and is completely immune to C++ SetBackdrop resets. + -------------------------------------------------------------------------- + if not GameTooltip._nanamiBG then + GameTooltip._nanamiBG = true + + local bgFrame = CreateFrame("Frame", "NanamiTooltipBG", UIParent) + bgFrame:SetFrameStrata("TOOLTIP") + bgFrame:SetFrameLevel(math.max(1, GameTooltip:GetFrameLevel()) - 1) + bgFrame:Hide() + GameTooltip._nanamiBGFrame = bgFrame + + local bg = bgFrame:CreateTexture(nil, "ARTWORK") + bg:SetTexture("Interface\\Buttons\\WHITE8X8") + local _Abg = SFrames.ActiveTheme + if _Abg and _Abg.panelBg then + bg:SetVertexColor(_Abg.panelBg[1], _Abg.panelBg[2], _Abg.panelBg[3], 0.95) + else + bg:SetVertexColor(0.08, 0.08, 0.08, 0.95) + end + bg:SetAllPoints(bgFrame) + GameTooltip._nanamiBGTex = bg + + end + + -------------------------------------------------------------------------- + -- Health bar: position, backdrop, texture, health text + -------------------------------------------------------------------------- + local barFont = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + + GameTooltipStatusBar:SetHeight(10) + GameTooltipStatusBar:ClearAllPoints() + GameTooltipStatusBar:SetPoint("BOTTOMLEFT", GameTooltip, "TOPLEFT", 4, 2) + GameTooltipStatusBar:SetPoint("BOTTOMRIGHT", GameTooltip, "TOPRIGHT", -4, 2) + GameTooltipStatusBar:SetStatusBarTexture(SFrames:GetTexture()) + + GameTooltipStatusBar.bg = GameTooltipStatusBar.bg or GameTooltipStatusBar:CreateTexture(nil, "BACKGROUND") + GameTooltipStatusBar.bg:SetTexture("Interface\\TARGETINGFRAME\\UI-StatusBar") + GameTooltipStatusBar.bg:SetVertexColor(.1, .1, 0, .8) + GameTooltipStatusBar.bg:SetAllPoints(true) + + if not GameTooltipStatusBar.backdrop then + local bd = CreateFrame("Frame", "SFramesTooltipStatusBarBD", GameTooltipStatusBar) + bd:SetPoint("TOPLEFT", GameTooltipStatusBar, "TOPLEFT", -3, 3) + bd:SetPoint("BOTTOMRIGHT", GameTooltipStatusBar, "BOTTOMRIGHT", 3, -3) + bd:SetBackdrop({ + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + bd:SetBackdropBorderColor(.8, .8, .8, 1) + GameTooltipStatusBar.backdrop = bd + end + + if not GameTooltipStatusBar.healthText then + local ht = GameTooltipStatusBar.backdrop:CreateFontString(nil, "DIALOG", "GameFontWhite") + ht:SetFont(barFont, 12, "OUTLINE") + ht:SetPoint("TOP", 0, 4) + ht:SetNonSpaceWrap(false) + GameTooltipStatusBar.healthText = ht + end + + if not GameTooltipStatusBar._origSetColor then + GameTooltipStatusBar._origSetColor = GameTooltipStatusBar.SetStatusBarColor + GameTooltipStatusBar.SetStatusBarColor = function() return end + end + + -------------------------------------------------------------------------- + -- Track mouseover name/level for health estimation + -------------------------------------------------------------------------- + local ttMouseName, ttMouseLevel + + local barEvents = CreateFrame("Frame", nil, GameTooltipStatusBar) + barEvents:RegisterEvent("UPDATE_MOUSEOVER_UNIT") + barEvents:SetScript("OnEvent", function() + ttMouseName = UnitName("mouseover") + ttMouseLevel = UnitLevel("mouseover") + end) + + local function TT_Abbreviate(val) + if val >= 1000000 then + return string.format("%.1fM", val / 1000000) + elseif val >= 10000 then + return string.format("%.1fK", val / 1000) + end + return tostring(math.floor(val)) + end + + local barThrottle = 0 + barEvents:SetScript("OnUpdate", function() + barThrottle = barThrottle + arg1 + if barThrottle < 0.1 then return end + barThrottle = 0 + + local hp = GameTooltipStatusBar:GetValue() + local _, hpmax = GameTooltipStatusBar:GetMinMaxValues() + if hpmax and hpmax > 0 then + if hpmax > 100 then + GameTooltipStatusBar.healthText:SetText( + TT_Abbreviate(hp) .. " / " .. TT_Abbreviate(hpmax)) + else + GameTooltipStatusBar.healthText:SetText( + string.format("%d%%", math.ceil(hp / hpmax * 100))) + end + else + GameTooltipStatusBar.healthText:SetText("") + end + end) + + local linesFormatted = false + + -------------------------------------------------------------------------- + -- Track tooltip owner to skip styling for world-map / pfQuest markers + -------------------------------------------------------------------------- + local ttOwner = nil + local orig_SetOwner = GameTooltip.SetOwner + GameTooltip.SetOwner = function(self, owner, anchor, xOff, yOff) + ttOwner = owner + return orig_SetOwner(self, owner, anchor, xOff, yOff) + end + + local function TT_IsMapMarkerTooltip() + if not ttOwner then return false end + if WorldMapFrame and WorldMapFrame:IsShown() then + local name = ttOwner.GetName and ttOwner:GetName() + if name and (string.find(name, "^pf") or string.find(name, "^WorldMap")) then + return true + end + local parent = ttOwner.GetParent and ttOwner:GetParent() + while parent do + if parent == WorldMapFrame or parent == WorldMapButton then + return true + end + parent = parent.GetParent and parent:GetParent() + end + end + return false + end + + local function TT_ShowBar(show) + if not GameTooltipStatusBar then return end + GameTooltipStatusBar:SetStatusBarTexture(SFrames:GetTexture()) + if show then + GameTooltipStatusBar:SetAlpha(1) + GameTooltipStatusBar:Show() + if GameTooltipStatusBar.backdrop then + GameTooltipStatusBar.backdrop:SetAlpha(1) + GameTooltipStatusBar.backdrop:Show() + end + else + GameTooltipStatusBar:SetAlpha(0) + if GameTooltipStatusBar.backdrop then + GameTooltipStatusBar.backdrop:SetAlpha(0) + end + if GameTooltipStatusBar.healthText then GameTooltipStatusBar.healthText:SetText("") end + end + end + + -- Sync the standalone bg frame to match GameTooltip's position/size + local function TT_SyncBGFrame() + local bf = GameTooltip._nanamiBGFrame + if not bf then return end + local w = GameTooltip:GetWidth() + local h = GameTooltip:GetHeight() + if w and h and w > 0 and h > 0 then + bf:SetWidth(w) + bf:SetHeight(h) + bf:ClearAllPoints() + bf:SetPoint("TOPLEFT", GameTooltip, "TOPLEFT", 0, 0) + bf:SetFrameLevel(math.max(1, GameTooltip:GetFrameLevel()) - 1) + bf:Show() + end + end + + -- OnShow: apply backdrop, sync bg frame, reset formatting flag + local orig_OnShow = GameTooltip:GetScript("OnShow") + local ttIsMapMarker = false + GameTooltip:SetScript("OnShow", function() + if orig_OnShow then orig_OnShow() end + linesFormatted = false + ttIsMapMarker = TT_IsMapMarkerTooltip() + TT_ApplyBackdrop(this) + TT_SyncBGFrame() + if not ttIsMapMarker then + TT_ShowBar(UnitExists("mouseover")) + end + end) + + -- OnUpdate: throttled backdrop/bar refresh and cursor tracking + local orig_OnUpdate = GameTooltip:GetScript("OnUpdate") + local ttThrottle = 0 + GameTooltip:SetScript("OnUpdate", function() + if orig_OnUpdate then orig_OnUpdate() end + + ttThrottle = ttThrottle + arg1 + if ttThrottle < 0.05 then return end + ttThrottle = 0 + + TT_ApplyBackdrop(this) + TT_SyncBGFrame() + if not ttIsMapMarker then + TT_ShowBar(UnitExists("mouseover")) + end + if not linesFormatted then + linesFormatted = true + if not ttIsMapMarker and UnitExists("mouseover") then + SFrames.FloatingTooltip:FormatLines(this) + end + end + + if SFramesDB and SFramesDB.tooltipMode == "CURSOR" and not ttIsMapMarker then + local x, y = GetCursorPosition() + local scale = UIParent:GetEffectiveScale() + if scale and scale > 0 then + this:ClearAllPoints() + this:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", (x / scale) + 16, (y / scale) - 16) + end + end + end) + + -- OnHide: hide bg frame, reset flag and owner tracking + local orig_OnHide = GameTooltip:GetScript("OnHide") + GameTooltip:SetScript("OnHide", function() + linesFormatted = false + ttOwner = nil + if GameTooltip._nanamiBGFrame then + GameTooltip._nanamiBGFrame:Hide() + end + if orig_OnHide then orig_OnHide() end + end) + + -- Tooltip Positioning logic + if not SFrames.FloatingTooltip.anchor then + local anchor = CreateFrame("Button", "SFramesTooltipAnchor", UIParent) + anchor:SetWidth(180) + anchor:SetHeight(60) + anchor:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -20, 100) + anchor:SetFrameStrata("HIGH") + anchor:EnableMouse(true) + anchor:SetMovable(true) + anchor:RegisterForDrag("LeftButton") + anchor:SetScript("OnDragStart", function() this:StartMoving() end) + anchor:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + if SFramesDB then + local _, _, _, x, y = this:GetPoint() + SFramesDB.tooltipX = x + SFramesDB.tooltipY = y + end + end) + + anchor:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + edgeSize = 1, + insets = {left=1, right=1, top=1, bottom=1} + }) + anchor:SetBackdropColor(0.08, 0.08, 0.08, 0.95) + anchor:SetBackdropBorderColor(0.4, 0.8, 0.4, 1) + + local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + + anchor.title = anchor:CreateFontString(nil, "OVERLAY") + anchor.title:SetFont(font, 13, "OUTLINE") + anchor.title:SetPoint("TOPLEFT", anchor, "TOPLEFT", 8, -8) + anchor.title:SetText("Tooltip 模拟位置") + anchor.title:SetTextColor(1, 1, 1) + + anchor.desc = anchor:CreateFontString(nil, "OVERLAY") + anchor.desc:SetFont(font, 11, "OUTLINE") + anchor.desc:SetPoint("TOPLEFT", anchor.title, "BOTTOMLEFT", 0, -4) + anchor.desc:SetText("拖拽我以调整[自定义]锚点。") + anchor.desc:SetTextColor(1, 0.82, 0) + anchor.desc:SetJustifyH("LEFT") + + anchor:Hide() + SFrames.FloatingTooltip.anchor = anchor + end + + if not SFrames.FloatingTooltip.hookedAnchor then + SFrames.FloatingTooltip.hookedAnchor = true + local orig_GameTooltip_SetDefaultAnchor = GameTooltip_SetDefaultAnchor + function GameTooltip_SetDefaultAnchor(tooltip, parent) + if SFramesDB and SFramesDB.tooltipMode == "CURSOR" then + tooltip:SetOwner(parent, "ANCHOR_NONE") + elseif SFramesDB and SFramesDB.tooltipMode == "CUSTOM" then + tooltip:SetOwner(parent, "ANCHOR_NONE") + tooltip:ClearAllPoints() + tooltip:SetPoint("BOTTOMRIGHT", "SFramesTooltipAnchor", "BOTTOMRIGHT", 0, 0) + else + orig_GameTooltip_SetDefaultAnchor(tooltip, parent) + end + end + end + SFrames.FloatingTooltip:ApplyConfig() + + -- WorldMapTooltip: raw textures on a child frame (SetBackdrop is unreliable) + if WorldMapTooltip and not WorldMapTooltip._nanamiBG then + WorldMapTooltip._nanamiBG = true + + local wmtBgFrame = CreateFrame("Frame", nil, WorldMapTooltip) + wmtBgFrame:SetAllPoints(WorldMapTooltip) + wmtBgFrame:SetFrameLevel(math.max(0, WorldMapTooltip:GetFrameLevel())) + + local bg = wmtBgFrame:CreateTexture(nil, "BACKGROUND") + bg:SetTexture("Interface\\Buttons\\WHITE8X8") + bg:SetVertexColor(0.05, 0.05, 0.05, 1) + bg:SetAllPoints(wmtBgFrame) + + local function WMTEdge(p1, r1, p2, r2, w, h) + local t = wmtBgFrame:CreateTexture(nil, "BORDER") + t:SetTexture("Interface\\Buttons\\WHITE8X8") + t:SetVertexColor(0.20, 0.20, 0.20, 1) + t:SetPoint(p1, WorldMapTooltip, r1) + t:SetPoint(p2, WorldMapTooltip, r2) + if w then t:SetWidth(w) end + if h then t:SetHeight(h) end + end + WMTEdge("TOPLEFT","TOPLEFT","TOPRIGHT","TOPRIGHT", nil, 1) + WMTEdge("BOTTOMLEFT","BOTTOMLEFT","BOTTOMRIGHT","BOTTOMRIGHT", nil, 1) + WMTEdge("TOPLEFT","TOPLEFT","BOTTOMLEFT","BOTTOMLEFT", 1, nil) + WMTEdge("TOPRIGHT","TOPRIGHT","BOTTOMRIGHT","BOTTOMRIGHT", 1, nil) + end + + if SFrames.ItemCompare and SFrames.ItemCompare.HookTooltips then + SFrames.ItemCompare:HookTooltips() + end +end + +function SFrames.FloatingTooltip:ApplyConfig() + if SFramesDB and SFramesDB.tooltipX and SFramesDB.tooltipY and self.anchor then + self.anchor:ClearAllPoints() + self.anchor:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", SFramesDB.tooltipX, SFramesDB.tooltipY) + end +end + +function SFrames.FloatingTooltip:ToggleAnchor(show) + if not self.anchor then return end + if show then + self.anchor:Show() + else + self.anchor:Hide() + end +end + +function SFrames.FloatingTooltip:FormatLines(tooltip) + local unit = "mouseover" + if not UnitExists(unit) then return end + + local nameLine = GameTooltipTextLeft1 + if not nameLine then return end + + local nameText = nameLine:GetText() + if nameText and string.find(nameText, "\n") then + nameText = string.gsub(nameText, "\n", " ") + nameLine:SetText(nameText) + end + + -------------------------------------------------------------------------- + -- Player units + -------------------------------------------------------------------------- + if UnitIsPlayer(unit) then + local classToken = TT_GetClassToken(unit) + + -- Class-color the name + if classToken then + local r, g, b = TT_GetClassColor(classToken) + if r then + nameLine:SetTextColor(r, g, b) + if GameTooltipStatusBar and GameTooltipStatusBar._origSetColor then + GameTooltipStatusBar._origSetColor(GameTooltipStatusBar, r, g, b) + end + end + end + + -- Fetch guild info for this player + local ttGuild, ttRankStr, ttRankIdx + if GetGuildInfo then + ttGuild, ttRankStr, ttRankIdx = GetGuildInfo(unit) + end + + -- Iterate existing lines: enhance guild line + color level + local numLines = tooltip:NumLines() + for i = 2, numLines do + local left = getglobal("GameTooltipTextLeft" .. i) + if left then + local txt = left:GetText() + if txt then + -- Guild line — append rank + if string.find(txt, "^<.*>$") then + if ttRankStr and ttRankStr ~= "" then + left:SetText(txt .. " - " .. ttRankStr) + end + left:SetTextColor(0.30, 0.90, 0.30) + end + + -- Level line + local _, _, lvlStr = string.find(txt, "^Level (%d+)") + if not lvlStr then + _, _, lvlStr = string.find(txt, "^等级 (%d+)") + end + local lvlNum = tonumber(lvlStr) + if lvlNum then + local lr, lg, lb = TT_DifficultyColor(lvlNum) + left:SetTextColor(lr, lg, lb) + end + end + end + end + + ----------------------------------------------------------------------- + -- Extra info lines (appended below existing content) + ----------------------------------------------------------------------- + + -- Guild info (if player is in a guild but Blizzard didn't show it) + if ttGuild and ttRankStr then + local foundGuild = false + for i = 2, numLines do + local left = getglobal("GameTooltipTextLeft" .. i) + if left then + local txt = left:GetText() or "" + if string.find(txt, "^<.*>") then + foundGuild = true + break + end + end + end + if not foundGuild then + tooltip:AddLine("<" .. ttGuild .. "> - " .. ttRankStr, 0.30, 0.90, 0.30) + end + end + + -- PvP Rank (text only; |T...|t inline textures not supported in 1.12) + if UnitPVPRank and GetPVPRankInfo then + local rank = UnitPVPRank(unit) + if rank and rank > 0 then + local rankName = GetPVPRankInfo(rank) + if rankName and rankName ~= "" then + tooltip:AddLine("军衔: " .. rankName, 1, 0.85, 0.35) + end + end + end + + -- PvP flag + faction + if UnitIsPVP and UnitIsPVP(unit) then + local fTag = UnitFactionGroup and UnitFactionGroup(unit) + if fTag then + local pvpHex = (fTag == "Horde") and "|cffff4444" or "|cff44bbff" + tooltip:AddLine(pvpHex .. "PvP " .. fTag .. "|r") + end + end + + -- Turtle WoW challenge modes (defensive checks) + if IsHardcore and IsHardcore(unit) then + tooltip:AddLine("|cffff3333[Hardcore]|r") + end + if C_TurtleWoW then + if C_TurtleWoW.IsHardcore and C_TurtleWoW.IsHardcore(unit) then + tooltip:AddLine("|cffff3333[Hardcore]|r") + end + if C_TurtleWoW.IsSurvivalist and C_TurtleWoW.IsSurvivalist(unit) then + tooltip:AddLine("|cff33ff33[Survivalist]|r") + end + if C_TurtleWoW.IsIronMan and C_TurtleWoW.IsIronMan(unit) then + tooltip:AddLine("|cffaaaaaa[Iron Man]|r") + end + end + if UnitIsTrivial and UnitIsTrivial(unit) then + tooltip:AddLine("|cff888888[休闲模式]|r") + end + + -------------------------------------------------------------------------- + -- NPC / Creature units: color health bar by reaction + -------------------------------------------------------------------------- + else + local reaction = UnitReaction(unit, "player") + if reaction then + local color = UnitReactionColor and UnitReactionColor[reaction] + if color and GameTooltipStatusBar and GameTooltipStatusBar._origSetColor then + GameTooltipStatusBar._origSetColor(GameTooltipStatusBar, color.r, color.g, color.b) + end + end + end + + -------------------------------------------------------------------------- + -- Target of mouseover (all units) + -------------------------------------------------------------------------- + local tgtUnit = unit .. "target" + if UnitExists(tgtUnit) then + local tgtName = UnitName(tgtUnit) + if tgtName then + local line + if UnitIsUnit(tgtUnit, "player") then + local _At = SFrames.ActiveTheme + local _accentHex = "ff6699" + if _At and _At.accentHex then _accentHex = _At.accentHex end + line = "|cffffd700目标: |r|cff" .. _accentHex .. ">> 你 <<|r" + elseif UnitIsPlayer(tgtUnit) then + local hex = TT_ClassHex(TT_GetClassToken(tgtUnit)) + line = "|cffffd700目标: |r" .. hex .. tgtName .. "|r" + else + line = "|cffffd700目标: |r|cffffffff" .. tgtName .. "|r" + end + tooltip:AddLine(line) + end + end + + tooltip:Show() +end + +-------------------------------------------------------------------------------- +-- Item Compare: show stat differences vs currently equipped item +-------------------------------------------------------------------------------- +SFrames.ItemCompare = {} + +local IC = SFrames.ItemCompare +local IC_STAT_ORDER = { + "STR","AGI","STA","INT","SPI", + "CRIT","TOHIT","RANGEDCRIT", + "SPELLCRIT","SPELLTOHIT", + "ATTACKPOWER","RANGEDATTACKPOWER", + "DMG","HEAL", + "DEFENSE","DODGE","PARRY","BLOCK","BLOCKVALUE", + "ARMOR", + "HEALTHREG","MANAREG", + "HEALTH","MANA", +} +local IC_STAT_NAMES = { + STR="力量", AGI="敏捷", STA="耐力", INT="智力", SPI="精神", + CRIT="暴击%", TOHIT="命中%", RANGEDCRIT="远程暴击%", + SPELLCRIT="法术暴击%", SPELLTOHIT="法术命中%", + ATTACKPOWER="攻强", RANGEDATTACKPOWER="远程攻强", + DMG="法伤", HEAL="治疗", + DEFENSE="防御", DODGE="闪避%", PARRY="招架%", BLOCK="格挡%", BLOCKVALUE="格挡值", + ARMOR="护甲", + HEALTHREG="生命/5秒", MANAREG="法力/5秒", + HEALTH="生命值", MANA="法力值", +} + +local IC_EQUIP_LOC_TO_SLOT = { + INVTYPE_HEAD = 1, INVTYPE_NECK = 2, INVTYPE_SHOULDER = 3, + INVTYPE_BODY = 4, INVTYPE_CHEST = 5, INVTYPE_ROBE = 5, + INVTYPE_WAIST = 6, INVTYPE_LEGS = 7, INVTYPE_FEET = 8, + INVTYPE_WRIST = 9, INVTYPE_HAND = 10, + INVTYPE_FINGER = 11, INVTYPE_TRINKET = 13, + INVTYPE_CLOAK = 15, + INVTYPE_WEAPON = 16, INVTYPE_2HWEAPON = 16, INVTYPE_WEAPONMAINHAND = 16, + INVTYPE_SHIELD = 17, INVTYPE_WEAPONOFFHAND = 17, INVTYPE_HOLDABLE = 17, + INVTYPE_RANGED = 18, INVTYPE_RANGEDRIGHT = 18, INVTYPE_THROWN = 18, + INVTYPE_RELIC = 18, INVTYPE_TABARD = 19, +} + +local function IC_GetLib() + if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("ItemBonusLib-1.0") then + return AceLibrary("ItemBonusLib-1.0") + end + return nil +end + +local function IC_GetEquipSlot(link) + if not link then return nil end + local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link) + if not equipLoc or equipLoc == "" then return nil end + return IC_EQUIP_LOC_TO_SLOT[equipLoc] +end + +local function IC_ScanBonuses(lib, link) + if not lib or not link then return nil end + local ok, result = pcall(function() return lib:ScanItem(link, true) end) + if ok and result then return result end + return nil +end + +local function IC_AppendCompare(tooltip, newBonuses, oldBonuses) + if not newBonuses then return end + local hasAny = false + local lines = {} + for _, key in ipairs(IC_STAT_ORDER) do + local nv = newBonuses[key] or 0 + local ov = (oldBonuses and oldBonuses[key]) or 0 + local diff = nv - ov + if diff ~= 0 then + local name = IC_STAT_NAMES[key] or key + local text + if diff > 0 then + text = "|cff00ff00+" .. diff .. " " .. name .. "|r" + else + text = "|cffff4444" .. diff .. " " .. name .. "|r" + end + table.insert(lines, text) + hasAny = true + end + end + if hasAny then + tooltip:AddLine(" ") + tooltip:AddLine("与当前装备对比:", 0.6, 0.8, 1) + for _, l in ipairs(lines) do + tooltip:AddLine(l) + end + tooltip:Show() + end +end + +local function IC_GetItemLevel(link) + if not link or not LibItem_Level then return nil end + local _, _, itemId = string.find(link, "item:(%d+)") + if itemId then return LibItem_Level[tonumber(itemId)] end + return nil +end + +local function IC_AppendItemLevel(tooltip, link) + if SFramesDB and SFramesDB.showItemLevel == false then return end + local ilvl = IC_GetItemLevel(link) + if ilvl and ilvl > 0 then + tooltip:AddLine("物品等级: " .. ilvl, 1, 0.82, 0) + end +end + +-------------------------------------------------------------------------------- +-- Sell Price Infrastructure +-------------------------------------------------------------------------------- +local function IC_GetItemIdFromLink(link) + if not link then return nil end + local _, _, id = string.find(link, "item:(%d+)") + if id then return tonumber(id) end + return nil +end + +local function IC_CacheSellPrice(itemId, copper) + if not itemId or not copper or copper <= 0 then return end + if not SFramesGlobalDB then SFramesGlobalDB = {} end + if not SFramesGlobalDB.sellPriceCache then SFramesGlobalDB.sellPriceCache = {} end + SFramesGlobalDB.sellPriceCache[itemId] = copper +end + +local function IC_GetSellPrice(itemId) + if not itemId then return nil end + if NanamiSellPriceDB then + local price = NanamiSellPriceDB[itemId] + if price and price > 0 then return price end + end + if ShaguTweaks and ShaguTweaks.SellValueDB then + local price = ShaguTweaks.SellValueDB[itemId] + if price and price > 0 then return price end + end + if SFramesGlobalDB and SFramesGlobalDB.sellPriceCache then + local price = SFramesGlobalDB.sellPriceCache[itemId] + if price and price > 0 then return price end + end + return nil +end + +local function IC_TryGetItemInfoSellPrice(link) + if not link then return nil end + local r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11 = GetItemInfo(link) + if r11 and type(r11) == "number" and r11 > 0 then + return r11 + end + return nil +end + +local function IC_ExtractLinkFromTooltipName(tooltip) + if not tooltip then return nil end + local left1 = _G[tooltip:GetName() .. "TextLeft1"] + if not left1 then return nil end + local name = left1:GetText() + if not name or name == "" then return nil end + if GetItemLinkByName then + local link = GetItemLinkByName(name) + if link then return link end + end + if ShaguTweaks and ShaguTweaks.GetItemLinkByName then + local link = ShaguTweaks.GetItemLinkByName(name) + if link then return link end + end + return nil +end + +local function IC_AddSellPrice(tooltip, link, count) + if not link then return end + if tooltip._nanamiSellPriceAdded then return end + if MerchantFrame and MerchantFrame:IsShown() then return end + local itemId = IC_GetItemIdFromLink(link) + local price = IC_GetSellPrice(itemId) + if not price then + price = IC_TryGetItemInfoSellPrice(link) + if price and itemId then IC_CacheSellPrice(itemId, price) end + end + if price and price > 0 then + count = count or 1 + if count < 1 then count = 1 end + tooltip._nanamiSellPriceAdded = true + SetTooltipMoney(tooltip, price * count) + tooltip:Show() + end +end + +-------------------------------------------------------------------------------- +local function IC_EnhanceTooltip(tooltip, link, count, skipSellPrice) + if not link then return end + IC_AppendItemLevel(tooltip, link) + if not SFramesDB or SFramesDB.itemCompare ~= false then + local lib = IC_GetLib() + if lib then + local eslot = IC_GetEquipSlot(link) + if eslot then + local newB = IC_ScanBonuses(lib, link) + local eqLink = GetInventoryItemLink("player", eslot) + local oldB = IC_ScanBonuses(lib, eqLink) + IC_AppendCompare(tooltip, newB, oldB) + end + end + end + if SFrames.GearScore and SFrames.GearScore.AddScoreToTooltip then + pcall(function() SFrames.GearScore:AddScoreToTooltip(tooltip, link) end) + end + if not skipSellPrice then + IC_AddSellPrice(tooltip, link, count) + end + tooltip:Show() +end + +function IC:HookTooltips() + if self.hooked then return end + self.hooked = true + + --------------------------------------------------------------------------- + -- OnHide cleanup: reset sell-price tracking flag + --------------------------------------------------------------------------- + local orig_OnHide_IC = GameTooltip:GetScript("OnHide") + GameTooltip:SetScript("OnHide", function() + this._nanamiSellPriceAdded = nil + if orig_OnHide_IC then orig_OnHide_IC() end + end) + + --------------------------------------------------------------------------- + -- Passive sell-price caching: intercept SetTooltipMoney calls while + -- processing a bag item at a merchant so we learn the unit price. + --------------------------------------------------------------------------- + local IC_PendingBagLink = nil + local IC_PendingBagCount = nil + local orig_SetTooltipMoney = SetTooltipMoney + SetTooltipMoney = function(frame, money, a1, a2, a3) + if orig_SetTooltipMoney then + orig_SetTooltipMoney(frame, money, a1, a2, a3) + end + if IC_PendingBagLink and money and money > 0 then + if frame == GameTooltip or frame == SFramesScanTooltip then + local itemId = IC_GetItemIdFromLink(IC_PendingBagLink) + local count = IC_PendingBagCount or 1 + if count < 1 then count = 1 end + if itemId then + IC_CacheSellPrice(itemId, math.floor(money / count)) + end + end + end + end + + --------------------------------------------------------------------------- + -- MERCHANT_SHOW: proactively scan all bag items to cache sell prices. + --------------------------------------------------------------------------- + local scanFrame = CreateFrame("Frame") + scanFrame:RegisterEvent("MERCHANT_SHOW") + scanFrame:SetScript("OnEvent", function() + for bag = 0, 4 do + local numSlots = GetContainerNumSlots(bag) or 0 + for slot = 1, numSlots do + local link = GetContainerItemLink(bag, slot) + if link then + local itemId = IC_GetItemIdFromLink(link) + if itemId and not IC_GetSellPrice(itemId) then + local infoPrice = IC_TryGetItemInfoSellPrice(link) + if infoPrice then + IC_CacheSellPrice(itemId, infoPrice) + elseif SFramesScanTooltip then + local _, cnt = GetContainerItemInfo(bag, slot) + cnt = cnt or 1 + IC_PendingBagLink = link + IC_PendingBagCount = cnt + pcall(function() + SFramesScanTooltip:SetOwner(UIParent, "ANCHOR_NONE") + SFramesScanTooltip:ClearLines() + SFramesScanTooltip:SetBagItem(bag, slot) + SFramesScanTooltip:Hide() + end) + IC_PendingBagLink = nil + IC_PendingBagCount = nil + end + end + end + end + end + end) + + --------------------------------------------------------------------------- + -- GameTooltip.SetBagItem + --------------------------------------------------------------------------- + local orig_SetBagItem = GameTooltip.SetBagItem + GameTooltip.SetBagItem = function(self, bag, slot) + self._nanamiSellPriceAdded = nil + local link = GetContainerItemLink(bag, slot) + local _, cnt = GetContainerItemInfo(bag, slot) + IC_PendingBagLink = link + IC_PendingBagCount = cnt or 1 + + local hasItem, hasCooldown, repairCost = orig_SetBagItem(self, bag, slot) + + IC_PendingBagLink = nil + IC_PendingBagCount = nil + + local moneyAlreadyShown = self.hasMoney + + pcall(function() + if link then + IC_EnhanceTooltip(GameTooltip, link, cnt, moneyAlreadyShown) + end + end) + return hasItem, hasCooldown, repairCost + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetInventoryItem + --------------------------------------------------------------------------- + local orig_SetInvItem = GameTooltip.SetInventoryItem + GameTooltip.SetInventoryItem = function(self, unit, slotId) + self._nanamiSellPriceAdded = nil + local hasItem, hasCooldown, repairCost = orig_SetInvItem(self, unit, slotId) + local moneyAlreadyShown = self.hasMoney + pcall(function() + if unit == "player" and slotId then + local link = GetInventoryItemLink("player", slotId) + if link then + IC_EnhanceTooltip(GameTooltip, link, nil, moneyAlreadyShown) + end + end + end) + return hasItem, hasCooldown, repairCost + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetMerchantItem + --------------------------------------------------------------------------- + local orig_SetMerchantItem = GameTooltip.SetMerchantItem + if orig_SetMerchantItem then + GameTooltip.SetMerchantItem = function(self, idx) + self._nanamiSellPriceAdded = nil + orig_SetMerchantItem(self, idx) + pcall(function() + local link = GetMerchantItemLink and GetMerchantItemLink(idx) + if link then IC_EnhanceTooltip(GameTooltip, link) end + end) + end + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetQuestItem — with fallback link extraction + --------------------------------------------------------------------------- + local orig_SetQuestItem = GameTooltip.SetQuestItem + if orig_SetQuestItem then + GameTooltip.SetQuestItem = function(self, qtype, idx) + self._nanamiSellPriceAdded = nil + orig_SetQuestItem(self, qtype, idx) + local moneyAlreadyShown = self.hasMoney + pcall(function() + local link + if GetQuestItemLink then link = GetQuestItemLink(qtype, idx) end + if not link then link = IC_ExtractLinkFromTooltipName(GameTooltip) end + if link then IC_EnhanceTooltip(GameTooltip, link, nil, moneyAlreadyShown) end + end) + end + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetQuestLogItem — with fallback link extraction + --------------------------------------------------------------------------- + local orig_SetQuestLogItem = GameTooltip.SetQuestLogItem + if orig_SetQuestLogItem then + GameTooltip.SetQuestLogItem = function(self, itype, idx) + self._nanamiSellPriceAdded = nil + orig_SetQuestLogItem(self, itype, idx) + local moneyAlreadyShown = self.hasMoney + pcall(function() + local link + if GetQuestLogItemLink then link = GetQuestLogItemLink(itype, idx) end + if not link then link = IC_ExtractLinkFromTooltipName(GameTooltip) end + if link then IC_EnhanceTooltip(GameTooltip, link, nil, moneyAlreadyShown) end + end) + end + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetLootItem + --------------------------------------------------------------------------- + local orig_SetLootItem = GameTooltip.SetLootItem + if orig_SetLootItem then + GameTooltip.SetLootItem = function(self, idx) + self._nanamiSellPriceAdded = nil + orig_SetLootItem(self, idx) + local moneyAlreadyShown = self.hasMoney + pcall(function() + local link = GetLootSlotLink and GetLootSlotLink(idx) + if link then IC_EnhanceTooltip(GameTooltip, link, nil, moneyAlreadyShown) end + end) + end + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetLootRollItem + --------------------------------------------------------------------------- + local orig_SetLootRollItem = GameTooltip.SetLootRollItem + if orig_SetLootRollItem then + GameTooltip.SetLootRollItem = function(self, rollId) + self._nanamiSellPriceAdded = nil + orig_SetLootRollItem(self, rollId) + local moneyAlreadyShown = self.hasMoney + pcall(function() + local link = GetLootRollItemLink and GetLootRollItemLink(rollId) + if link then IC_EnhanceTooltip(GameTooltip, link, nil, moneyAlreadyShown) end + end) + end + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetCraftItem + --------------------------------------------------------------------------- + local orig_SetCraftItem = GameTooltip.SetCraftItem + if orig_SetCraftItem then + GameTooltip.SetCraftItem = function(self, skill, slot) + self._nanamiSellPriceAdded = nil + orig_SetCraftItem(self, skill, slot) + local moneyAlreadyShown = self.hasMoney + pcall(function() + local link = GetCraftReagentItemLink and GetCraftReagentItemLink(skill, slot) + if link then IC_EnhanceTooltip(GameTooltip, link, nil, moneyAlreadyShown) end + end) + end + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetTradeSkillItem + --------------------------------------------------------------------------- + local orig_SetTradeSkillItem = GameTooltip.SetTradeSkillItem + if orig_SetTradeSkillItem then + GameTooltip.SetTradeSkillItem = function(self, skillIndex, reagentIndex) + self._nanamiSellPriceAdded = nil + orig_SetTradeSkillItem(self, skillIndex, reagentIndex) + local moneyAlreadyShown = self.hasMoney + pcall(function() + local link + if reagentIndex then + if GetTradeSkillReagentItemLink then + link = GetTradeSkillReagentItemLink(skillIndex, reagentIndex) + end + else + if GetTradeSkillItemLink then + link = GetTradeSkillItemLink(skillIndex) + end + end + if link then IC_EnhanceTooltip(GameTooltip, link, nil, moneyAlreadyShown) end + end) + end + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetAuctionItem + --------------------------------------------------------------------------- + local orig_SetAuctionItem = GameTooltip.SetAuctionItem + if orig_SetAuctionItem then + GameTooltip.SetAuctionItem = function(self, atype, idx) + self._nanamiSellPriceAdded = nil + orig_SetAuctionItem(self, atype, idx) + local moneyAlreadyShown = self.hasMoney + pcall(function() + local _, _, cnt = GetAuctionItemInfo and GetAuctionItemInfo(atype, idx) + local link = GetAuctionItemLink and GetAuctionItemLink(atype, idx) + if link then IC_EnhanceTooltip(GameTooltip, link, cnt, moneyAlreadyShown) end + end) + end + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetTradePlayerItem + --------------------------------------------------------------------------- + local orig_SetTradePlayerItem = GameTooltip.SetTradePlayerItem + if orig_SetTradePlayerItem then + GameTooltip.SetTradePlayerItem = function(self, idx) + self._nanamiSellPriceAdded = nil + orig_SetTradePlayerItem(self, idx) + local moneyAlreadyShown = self.hasMoney + pcall(function() + local link = GetTradePlayerItemLink and GetTradePlayerItemLink(idx) + if link then IC_EnhanceTooltip(GameTooltip, link, nil, moneyAlreadyShown) end + end) + end + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetTradeTargetItem + --------------------------------------------------------------------------- + local orig_SetTradeTargetItem = GameTooltip.SetTradeTargetItem + if orig_SetTradeTargetItem then + GameTooltip.SetTradeTargetItem = function(self, idx) + self._nanamiSellPriceAdded = nil + orig_SetTradeTargetItem(self, idx) + local moneyAlreadyShown = self.hasMoney + pcall(function() + local link = GetTradeTargetItemLink and GetTradeTargetItemLink(idx) + if link then IC_EnhanceTooltip(GameTooltip, link, nil, moneyAlreadyShown) end + end) + end + end + + --------------------------------------------------------------------------- + -- GameTooltip.SetInboxItem + --------------------------------------------------------------------------- + local orig_SetInboxItem = GameTooltip.SetInboxItem + if orig_SetInboxItem then + GameTooltip.SetInboxItem = function(self, mailID, attachIdx) + self._nanamiSellPriceAdded = nil + orig_SetInboxItem(self, mailID, attachIdx) + local moneyAlreadyShown = self.hasMoney + pcall(function() + local link = IC_ExtractLinkFromTooltipName(GameTooltip) + if link then IC_EnhanceTooltip(GameTooltip, link, nil, moneyAlreadyShown) end + end) + end + end + + --------------------------------------------------------------------------- + -- SetItemRef (chat item links) + --------------------------------------------------------------------------- + local orig_SetItemRef = SetItemRef + if orig_SetItemRef then + SetItemRef = function(link, text, button) + orig_SetItemRef(link, text, button) + if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then return end + pcall(function() + local _, _, itemStr = string.find(link or "", "(item:[%-?%d:]+)") + if itemStr then + ItemRefTooltip._nanamiSellPriceAdded = nil + local itemId = IC_GetItemIdFromLink(itemStr) + local price = IC_GetSellPrice(itemId) + if not price then + price = IC_TryGetItemInfoSellPrice(itemStr) + if price and itemId then IC_CacheSellPrice(itemId, price) end + end + if price and price > 0 and not ItemRefTooltip.hasMoney then + SetTooltipMoney(ItemRefTooltip, price) + ItemRefTooltip:Show() + end + end + end) + end + end +end diff --git a/Trade.lua b/Trade.lua new file mode 100644 index 0000000..6e2bdd2 --- /dev/null +++ b/Trade.lua @@ -0,0 +1,1268 @@ +local AddOnName = "Nanami-UI" +SFrames = SFrames or {} + +local TradeUI = CreateFrame("Frame", "SFramesTradeUI", UIParent) +TradeUI:RegisterEvent("TRADE_SHOW") +TradeUI:RegisterEvent("TRADE_CLOSED") +TradeUI:RegisterEvent("TRADE_UPDATE") +TradeUI:RegisterEvent("TRADE_PLAYER_ITEM_CHANGED") +TradeUI:RegisterEvent("TRADE_TARGET_ITEM_CHANGED") +TradeUI:RegisterEvent("TRADE_ACCEPT_UPDATE") +TradeUI:RegisterEvent("TRADE_MONEY_CHANGED") +TradeUI:RegisterEvent("PLAYER_TRADE_MONEY") +TradeUI:RegisterEvent("UI_INFO_MESSAGE") +TradeUI:RegisterEvent("CHAT_MSG_SYSTEM") + +local L = { + NOT_TRADED = "不会被交易", + WHISPER_CHECK = "发送清单", + CONFIRMED = "已确认", + WAITING = "等待中...", +} + +local TRADE_DATA = { + active = false, + playerItems = {}, + targetItems = {}, + playerMoney = 0, + targetMoney = 0, + targetName = "", + playerAccepted = false, + targetAccepted = false, +} + +SFramesDB = SFramesDB or {} + +-------------------------------------------------------------------------------- +-- Theme: Pink Cat-Paw (matching GameMenu / Nanami-UI) +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + labelDim = { 0.7, 0.5, 0.6 }, + tradeBg = { 0.10, 0.22, 0.12, 0.95 }, + tradeBorder = { 0.30, 0.70, 0.35, 0.90 }, + tradeText = { 0.7, 1, 0.75 }, + moneyBg = { 0.08, 0.04, 0.06, 0.85 }, + moneyBorder = { 0.40, 0.25, 0.35, 0.6 }, + confirmOverlay = { 0.15, 0.85, 0.25, 0.55 }, + confirmBorder = { 0.30, 1.0, 0.40, 0.95 }, + confirmText = { 0.3, 1, 0.4 }, + statusConfirmBg = { 0.08, 0.35, 0.12, 0.92 }, + statusConfirmBd = { 0.25, 0.95, 0.35, 0.95 }, + statusWaitBg = { 0.15, 0.08, 0.12, 0.8 }, + statusWaitBd = { 0.45, 0.28, 0.38, 0.6 }, + statusWaitText = { 0.6, 0.45, 0.55 }, +}) + +local FRAME_W = 440 +local SLOT_W = 188 +local SLOT_H = 42 +local SLOT_GAP = 2 +local SIDE_PAD = 16 +local HEADER_H = 46 +local BOTTOM_H = 54 +local NOT_TRADED_GAP = 38 +local STATUS_BAR_H = 22 +-- FRAME_H computed: HEADER_H+8 + 6*SLOT_H+5*GAP + NOT_TRADED_GAP + SLOT_H + 6 + STATUS_BAR_H + 6 + BOTTOM_H +local FRAME_H = HEADER_H + 8 + 6 * SLOT_H + 5 * SLOT_GAP + NOT_TRADED_GAP + SLOT_H + 6 + STATUS_BAR_H + 6 + BOTTOM_H + +local DEFAULT_SLOT_BORDER = { 0.25, 0.25, 0.3, 0.8 } + +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +-------------------------------------------------------------------------------- +-- Data logic +-------------------------------------------------------------------------------- +local function SaveTradeState() + if not TradeFrame or not TradeFrame:IsVisible() then return end + + local pItems = {} + for i = 1, 6 do + local link = nil + if GetTradePlayerItemLink then + link = GetTradePlayerItemLink(i) + end + if not link and GetTradePlayerItemInfo then + local name, texture, numItems = GetTradePlayerItemInfo(i) + if name and name ~= "" then + link = name + end + end + if link then + local _, _, numItems = GetTradePlayerItemInfo(i) + table.insert(pItems, { link = link, count = numItems or 1 }) + end + end + if table.getn(pItems) > 0 then + TRADE_DATA.playerItems = pItems + end + + local tItems = {} + for i = 1, 6 do + local link = nil + if GetTradeTargetItemLink then + link = GetTradeTargetItemLink(i) + end + if not link and GetTradeTargetItemInfo then + local name, texture, numItems = GetTradeTargetItemInfo(i) + if name and name ~= "" then + link = name + end + end + if link then + local _, _, numItems = GetTradeTargetItemInfo(i) + table.insert(tItems, { link = link, count = numItems or 1 }) + end + end + if table.getn(tItems) > 0 then + TRADE_DATA.targetItems = tItems + end + + local pm = GetPlayerTradeMoney and GetPlayerTradeMoney() or 0 + local tm = GetTargetTradeMoney and GetTargetTradeMoney() or 0 + if pm >= 0 and pm > TRADE_DATA.playerMoney then + TRADE_DATA.playerMoney = pm + end + if tm >= 0 and tm > TRADE_DATA.targetMoney then + TRADE_DATA.targetMoney = tm + end + + local name = UnitName("NPC") + if name and name ~= "" then TRADE_DATA.targetName = name end +end + +local function FormatMoneyZH(copper) + if not copper or copper == 0 then return nil end + local g = math.floor(copper / 10000) + local s = math.floor((copper - (g * 10000)) / 100) + local c = math.mod(copper, 100) + local text = "" + if g > 0 then text = text .. g .. "g " end + if s > 0 then text = text .. s .. "s " end + if c > 0 then text = text .. c .. "c " end + return text +end + +local function SendLine(msg, channel, target) + if channel == "WHISPER" then + if target and target ~= "" and target ~= "Unknown" then + SendChatMessage(msg, "WHISPER", nil, target) + end + else + SendChatMessage(msg, channel) + end +end + +local function SendTradeWhisper() + if not SFramesDB.TradeWhisperEnable then return end + local target = TRADE_DATA.targetName + local channel = SFramesDB.TradeWhisperChannel or "WHISPER" + local outLines = {} + + local playerMoneyStr = FormatMoneyZH(TRADE_DATA.playerMoney) + local targetMoneyStr = FormatMoneyZH(TRADE_DATA.targetMoney) + + local giveItems = "" + for _, item in ipairs(TRADE_DATA.playerItems) do + giveItems = giveItems .. item.link .. (item.count > 1 and ("x" .. item.count) or "") .. " " + end + local getItems = "" + for _, item in ipairs(TRADE_DATA.targetItems) do + getItems = getItems .. item.link .. (item.count > 1 and ("x" .. item.count) or "") .. " " + end + + if not playerMoneyStr and giveItems == "" and not targetMoneyStr and getItems == "" then + return + end + + table.insert(outLines, "=== 濞存嚎鍊栧Σ妤冩媼閺夎法绉?===") + if playerMoneyStr then table.insert(outLines, "濞寸姵锚閸ゎ參鏌岄幋婵堫伈: " .. playerMoneyStr) end + if giveItems ~= "" then table.insert(outLines, "濞寸姵锚閸ゎ參鎮ч埡浣规儌: " .. giveItems) end + if targetMoneyStr then table.insert(outLines, "闁衡偓閹澘绠梺鍙夊灥缁? " .. targetMoneyStr) end + if getItems ~= "" then table.insert(outLines, "闁衡偓閹澘绠柣妞绘櫅閹? " .. getItems) end + + for _, line in ipairs(outLines) do + SendLine(line, channel, target) + end +end + +local function ClearTradeData() + TRADE_DATA.playerItems = {} + TRADE_DATA.targetItems = {} + TRADE_DATA.playerMoney = 0 + TRADE_DATA.targetMoney = 0 +end + +local function IsTradeCompleteMsg(msg) + if not msg then return false end + if string.find(msg, "Trade successful") then return true end + if string.find(msg, "Trade complete") then return true end + if string.find(msg, "Trade complete") then return true end + return false +end + +local sfTradeRefreshing = false +local function ForceRefreshTradeVisuals() + if not TradeFrame or not TradeFrame:IsVisible() then return end + if sfTradeRefreshing then return end + if TradeFrame_Update then + sfTradeRefreshing = true + pcall(function() TradeFrame_Update() end) + sfTradeRefreshing = false + end +end + +TradeUI:SetScript("OnEvent", function() + if event == "TRADE_SHOW" then + TRADE_DATA.active = true + TRADE_DATA.targetName = UnitName("NPC") or "" + TRADE_DATA.playerItems = {} + TRADE_DATA.targetItems = {} + TRADE_DATA.playerMoney = 0 + TRADE_DATA.targetMoney = 0 + TRADE_DATA.playerAccepted = false + TRADE_DATA.targetAccepted = false + SaveTradeState() + ForceRefreshTradeVisuals() + elseif event == "TRADE_PLAYER_ITEM_CHANGED" or event == "TRADE_TARGET_ITEM_CHANGED" then + TRADE_DATA.playerAccepted = false + TRADE_DATA.targetAccepted = false + SaveTradeState() + ForceRefreshTradeVisuals() + elseif event == "TRADE_UPDATE" then + SaveTradeState() + ForceRefreshTradeVisuals() + elseif event == "TRADE_ACCEPT_UPDATE" then + TRADE_DATA.playerAccepted = (arg1 and arg1 == 1) + TRADE_DATA.targetAccepted = (arg2 and arg2 == 1) + SaveTradeState() + ForceRefreshTradeVisuals() + elseif event == "TRADE_MONEY_CHANGED" or event == "PLAYER_TRADE_MONEY" then + SaveTradeState() + ForceRefreshTradeVisuals() + elseif event == "TRADE_CLOSED" then + TRADE_DATA.active = false + TRADE_DATA.playerAccepted = false + TRADE_DATA.targetAccepted = false + elseif event == "UI_INFO_MESSAGE" then + if IsTradeCompleteMsg(arg1) then + SendTradeWhisper() + ClearTradeData() + end + elseif event == "CHAT_MSG_SYSTEM" then + if IsTradeCompleteMsg(arg1) then + SendTradeWhisper() + ClearTradeData() + end + end +end) + +-------------------------------------------------------------------------------- +-- UI Helpers (matching GameMenu / Nanami-UI style) +-------------------------------------------------------------------------------- +local function SetRoundBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + local bg = bgColor or T.panelBg + local bd = borderColor or T.panelBorder + frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) + frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) +end + +local function CreateShadow(parent, size) + local s = CreateFrame("Frame", nil, parent) + local sz = size or 4 + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz) + s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0)) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.55) + s:SetBackdropBorderColor(0, 0, 0, 0.4) + return s +end + +-------------------------------------------------------------------------------- +-- Tooltip scanner for item level +-------------------------------------------------------------------------------- +local tooltipScanner = CreateFrame("GameTooltip", "SFramesTradeTooltipScan", nil, "GameTooltipTemplate") +tooltipScanner:SetOwner(UIParent, "ANCHOR_NONE") + +local function ScanItemLevelFromTooltip() + for i = 2, tooltipScanner:NumLines() do + local line = _G["SFramesTradeTooltipScanTextLeft" .. i] + if line then + local text = line:GetText() + if text then + local _, _, ilvl = string.find(text, "(%d+)") + if string.find(text, "Item Level") or string.find(text, "iLvl") or string.find(text, "ilvl") or string.find(text, "鐗╁搧绛夌骇") then + if ilvl then return tonumber(ilvl) end + end + end + end + end + return nil +end + +local function GetTradePlayerItemLevel(slot) + tooltipScanner:SetOwner(UIParent, "ANCHOR_NONE") + tooltipScanner:ClearLines() + local ok = pcall(function() tooltipScanner:SetTradePlayerItem(slot) end) + if ok then return ScanItemLevelFromTooltip() end + local link = GetTradePlayerItemLink and GetTradePlayerItemLink(slot) + if link then + tooltipScanner:SetOwner(UIParent, "ANCHOR_NONE") + tooltipScanner:ClearLines() + ok = pcall(function() tooltipScanner:SetHyperlink(link) end) + if ok then return ScanItemLevelFromTooltip() end + end + return nil +end + +local function GetTradeTargetItemLevel(slot) + tooltipScanner:SetOwner(UIParent, "ANCHOR_NONE") + tooltipScanner:ClearLines() + local ok = pcall(function() tooltipScanner:SetTradeTargetItem(slot) end) + if ok then return ScanItemLevelFromTooltip() end + local link = GetTradeTargetItemLink and GetTradeTargetItemLink(slot) + if link then + tooltipScanner:SetOwner(UIParent, "ANCHOR_NONE") + tooltipScanner:ClearLines() + ok = pcall(function() tooltipScanner:SetHyperlink(link) end) + if ok then return ScanItemLevelFromTooltip() end + end + return nil +end + +-------------------------------------------------------------------------------- +-- Quality color from item link +-------------------------------------------------------------------------------- +local function GetQualityColorFromLink(link) + if not link then return nil, nil, nil end + local _, _, hex = string.find(link, "|c(%x+)|H") + if hex and string.len(hex) == 8 then + local r = tonumber(string.sub(hex, 3, 4), 16) / 255 + local g = tonumber(string.sub(hex, 5, 6), 16) / 255 + local b = tonumber(string.sub(hex, 7, 8), 16) / 255 + return r, g, b + end + return nil, nil, nil +end + +local function GetQualityColorFromRarity(rarity) + if type(rarity) ~= "number" then return nil, nil, nil end + if not GetItemQualityColor then return nil, nil, nil end + local ok, r, g, b = pcall(function() return GetItemQualityColor(rarity) end) + if ok and r and g and b then + return r, g, b + end + return nil, nil, nil +end +local function ResolveTradeItemQuality(link, quality, name) + if type(quality) == "number" then + return quality + end + if GetItemInfo then + if link then + local _, _, q = GetItemInfo(link) + if type(q) == "number" then return q end + end + if name then + local _, _, q = GetItemInfo(name) + if type(q) == "number" then return q end + end + end + return nil +end + +local function IsCommonOrPoor(link) + if not link then return true end + local _, _, hex = string.find(link, "|c(%x+)|H") + if hex then + local hexLower = string.lower(hex) + return hexLower == "ffffffff" or hexLower == "ff9d9d9d" + end + return true +end + +-------------------------------------------------------------------------------- +-- Right-click to remove player trade items +-------------------------------------------------------------------------------- +local function HookSingleTradeSlot(slotIndex) + local itemBtn = _G["TradePlayerItem" .. slotIndex .. "ItemButton"] + if not itemBtn or itemBtn.sfRightClickHooked then return end + itemBtn.sfRightClickHooked = true + + itemBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp") + + local origScript = itemBtn:GetScript("OnClick") + itemBtn:SetScript("OnClick", function() + if arg1 == "RightButton" then + local hasItem = false + if GetTradePlayerItemLink then + local link = GetTradePlayerItemLink(slotIndex) + if link then hasItem = true end + end + if not hasItem then + local name = GetTradePlayerItemInfo(slotIndex) + if name and name ~= "" then hasItem = true end + end + if hasItem then + ClearCursor() + ClickTradeButton(slotIndex) + return + end + end + if origScript then origScript() end + end) +end + +local function HookTradeItemRightClick() + for i = 1, 6 do + HookSingleTradeSlot(i) + end +end + +-------------------------------------------------------------------------------- +-- Re-hide Blizzard visuals (called every TradeFrame_Update) +-------------------------------------------------------------------------------- +local function ReHideBlizzardSlot(itemPrefix, index) + local btnName = itemPrefix .. index .. "ItemButton" + local icon = _G[btnName .. "IconTexture"] + if icon then icon:SetAlpha(0); icon:Hide() end + local nt = _G[btnName .. "NormalTexture"] + if nt then nt:SetAlpha(0); nt:Hide() end + local slot = _G[btnName .. "SlotTexture"] + if slot then slot:SetAlpha(0); slot:Hide() end + local cnt = _G[btnName .. "Count"] + if cnt then cnt:SetAlpha(0) end + local bgSlot = _G[itemPrefix .. index .. "ItemButtonSlotTexture"] + if bgSlot then bgSlot:SetAlpha(0); bgSlot:Hide() end + local bgSlot2 = _G[itemPrefix .. index .. "SlotTexture"] + if bgSlot2 then bgSlot2:SetAlpha(0); bgSlot2:Hide() end + local bgSlot3 = _G[itemPrefix .. index .. "ItemButtonBackground"] + if bgSlot3 then bgSlot3:SetAlpha(0); bgSlot3:Hide() end + local nf = _G[itemPrefix .. index .. "NameFrame"] + if nf then nf:SetAlpha(0); nf:Hide() end +end + +-------------------------------------------------------------------------------- +-- Skin Trade Frame +-------------------------------------------------------------------------------- +local function SkinTradeFrame() + if TradeFrame.sfSkinned then return end + TradeFrame.sfSkinned = true + + TradeFrame:SetMovable(true) + TradeFrame:EnableMouse(true) + TradeFrame:RegisterForDrag("LeftButton") + TradeFrame:SetScript("OnDragStart", function() this:StartMoving() end) + TradeFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + -- Hide ALL default textures + local regions = { TradeFrame:GetRegions() } + local bottomTexts = {} + for _, r in ipairs(regions) do + if r:IsObjectType("Texture") then + r:SetTexture(nil) + r:SetAlpha(0) + elseif r:IsObjectType("FontString") then + local text = r:GetText() + if text and (string.find(text, "不会被交易") or string.find(text, "Will not be traded") or text == L.NOT_TRADED) then + table.insert(bottomTexts, r) + end + end + end + TradeFrame.sfBottomTexts = bottomTexts + + TradeFrame:SetWidth(FRAME_W) + TradeFrame:SetHeight(FRAME_H) + + -- Main backdrop - Nanami-UI rounded style (matching GameMenu) + SetRoundBackdrop(TradeFrame, T.panelBg, T.panelBorder) + CreateShadow(TradeFrame, 5) + + -- Header separator + local headerSep = TradeFrame:CreateTexture(nil, "ARTWORK") + headerSep:SetTexture("Interface\\Buttons\\WHITE8X8") + headerSep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + headerSep:SetHeight(1) + headerSep:SetPoint("TOPLEFT", TradeFrame, "TOPLEFT", 4, -HEADER_H) + headerSep:SetPoint("TOPRIGHT", TradeFrame, "TOPRIGHT", -4, -HEADER_H) + + -- Center vertical divider (pink tinted) + local divLine = TradeFrame:CreateTexture(nil, "ARTWORK") + divLine:SetTexture("Interface\\Buttons\\WHITE8X8") + divLine:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + divLine:SetWidth(1) + divLine:SetPoint("TOP", TradeFrame, "TOP", 0, -(HEADER_H + 2)) + divLine:SetPoint("BOTTOM", TradeFrame, "BOTTOM", 0, BOTTOM_H + 2) + + -- Bottom separator + local bottomSep = TradeFrame:CreateTexture(nil, "ARTWORK") + bottomSep:SetTexture("Interface\\Buttons\\WHITE8X8") + bottomSep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + bottomSep:SetHeight(1) + bottomSep:SetPoint("BOTTOMLEFT", TradeFrame, "BOTTOMLEFT", 4, BOTTOM_H) + bottomSep:SetPoint("BOTTOMRIGHT", TradeFrame, "BOTTOMRIGHT", -4, BOTTOM_H) + + -- Player name (pink/gold tint) + TradeFramePlayerNameText:ClearAllPoints() + TradeFramePlayerNameText:SetPoint("TOPLEFT", TradeFrame, "TOPLEFT", SIDE_PAD, -8) + TradeFramePlayerNameText:SetFont(GetFont(), 12, "OUTLINE") + TradeFramePlayerNameText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + + -- Target name + TradeFrameRecipientNameText:ClearAllPoints() + TradeFrameRecipientNameText:SetPoint("TOPRIGHT", TradeFrame, "TOPRIGHT", -SIDE_PAD, -8) + TradeFrameRecipientNameText:SetFont(GetFont(), 12, "OUTLINE") + TradeFrameRecipientNameText:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + + if TradeFrameRecipientPortrait then TradeFrameRecipientPortrait:Hide() end + if TradeFramePlayerPortrait then TradeFramePlayerPortrait:Hide() end + + -- Confirmation status bars (created here, positioned after item layout) + local function CreateStatusBar(parent, anchorSide) + local bar = CreateFrame("Frame", nil, parent) + bar:SetWidth(SLOT_W) + bar:SetHeight(STATUS_BAR_H) + bar:SetFrameLevel(parent:GetFrameLevel() + 5) + SetRoundBackdrop(bar, T.statusWaitBg, T.statusWaitBd) + + local icon = bar:CreateFontString(nil, "OVERLAY") + icon:SetFont(GetFont(), 13, "OUTLINE") + icon:SetPoint("LEFT", bar, "LEFT", 8, 0) + bar.sfIcon = icon + + local label = bar:CreateFontString(nil, "OVERLAY") + label:SetFont(GetFont(), 12, "OUTLINE") + label:SetPoint("LEFT", icon, "RIGHT", 4, 0) + label:SetPoint("RIGHT", bar, "RIGHT", -6, 0) + label:SetJustifyH(anchorSide) + bar.sfLabel = label + + local glow = bar:CreateTexture(nil, "BACKGROUND") + glow:SetTexture("Interface\\Buttons\\WHITE8X8") + glow:SetAllPoints(bar) + glow:SetAlpha(0) + bar.sfGlow = glow + + bar.sfElapsed = 0 + bar:SetScript("OnUpdate", function() + if not this.sfConfirmed then return end + this.sfElapsed = (this.sfElapsed or 0) + arg1 + local a = 0.35 + 0.2 * math.sin(this.sfElapsed * 3.5) + if this.sfGlow then this.sfGlow:SetAlpha(a) end + end) + + bar:Hide() + return bar + end + + local playerStatusBar = CreateStatusBar(TradeFrame, "LEFT") + TradeFrame.sfPlayerStatusBar = playerStatusBar + + local targetStatusBar = CreateStatusBar(TradeFrame, "LEFT") + TradeFrame.sfTargetStatusBar = targetStatusBar + + -- Money frames + TradePlayerInputMoneyFrame:ClearAllPoints() + TradePlayerInputMoneyFrame:SetPoint("TOPLEFT", TradeFrame, "TOPLEFT", SIDE_PAD, -24) + + local pMoneyBg = CreateFrame("Frame", nil, TradeFrame) + pMoneyBg:SetPoint("TOPLEFT", TradePlayerInputMoneyFrame, "TOPLEFT", -3, 3) + pMoneyBg:SetPoint("BOTTOMRIGHT", TradePlayerInputMoneyFrame, "BOTTOMRIGHT", 3, -3) + pMoneyBg:SetFrameLevel(math.max(TradePlayerInputMoneyFrame:GetFrameLevel() - 1, 0)) + SetRoundBackdrop(pMoneyBg, T.moneyBg, T.moneyBorder) + + TradeRecipientMoneyFrame:ClearAllPoints() + TradeRecipientMoneyFrame:SetPoint("TOPRIGHT", TradeFrame, "TOPRIGHT", -SIDE_PAD, -24) + + local rMoneyBg = CreateFrame("Frame", nil, TradeFrame) + rMoneyBg:SetPoint("TOPLEFT", TradeRecipientMoneyFrame, "TOPLEFT", -3, 3) + rMoneyBg:SetPoint("BOTTOMRIGHT", TradeRecipientMoneyFrame, "BOTTOMRIGHT", 3, -3) + rMoneyBg:SetFrameLevel(math.max(TradeRecipientMoneyFrame:GetFrameLevel() - 1, 0)) + SetRoundBackdrop(rMoneyBg, T.moneyBg, T.moneyBorder) + + -- Style money input text + local moneyEditNames = { "Gold", "Silver", "Copper" } + for _, suffix in ipairs(moneyEditNames) do + local eb = _G["TradePlayerInputMoneyFrame" .. suffix] + if eb and eb.SetFont then + eb:SetFont(GetFont(), 12, "OUTLINE") + eb:SetTextColor(1, 1, 1, 1) + end + local btn = _G["TradePlayerInputMoneyFrame" .. suffix .. "Button"] + if btn then + local icon = btn:GetNormalTexture() + if icon then icon:SetVertexColor(1, 1, 1, 1) end + end + local rBtn = _G["TradeRecipientMoneyFrame" .. suffix .. "Button"] + if rBtn then + local rText = _G["TradeRecipientMoneyFrame" .. suffix .. "ButtonText"] + if rText and rText.SetFont then + rText:SetFont(GetFont(), 11, "OUTLINE") + rText:SetTextColor(1, 1, 1, 1) + end + end + end + + ---------------------------------------------------------------------------- + -- Item slots - fully custom, Blizzard button is invisible click receiver + ---------------------------------------------------------------------------- + local function HideBlizzardButton(itemBtn) + local btnName = itemBtn:GetName() + + -- Kill every known named child texture by explicit name + local suffixes = { "IconTexture", "NormalTexture", "SlotTexture", "Count" } + for _, suf in ipairs(suffixes) do + local obj = _G[btnName .. suf] + if obj then + if obj.SetTexture then obj:SetTexture(nil) end + if obj.SetAlpha then obj:SetAlpha(0) end + if obj.SetTextColor then obj:SetTextColor(0,0,0,0) end + if obj.Hide then obj:Hide() end + end + end + + -- Kill template textures via API + if itemBtn.GetNormalTexture then + local nt = itemBtn:GetNormalTexture() + if nt then nt:SetTexture(nil); nt:SetAlpha(0) end + end + if itemBtn.GetPushedTexture then + local pt = itemBtn:GetPushedTexture() + if pt then pt:SetTexture(nil); pt:SetAlpha(0) end + end + if itemBtn.GetHighlightTexture then + local ht = itemBtn:GetHighlightTexture() + if ht then ht:SetTexture(nil); ht:SetAlpha(0) end + end + + -- Kill all remaining child regions + local regions = { itemBtn:GetRegions() } + for _, r in ipairs(regions) do + if r then + if r.SetTexture then r:SetTexture(nil) end + if r.SetAlpha then r:SetAlpha(0) end + if r.SetTextColor then r:SetTextColor(0,0,0,0) end + if r.Hide then r:Hide() end + end + end + + itemBtn:SetBackdrop(nil) + end + + local function CreateSfSlot(parent) + local slot = CreateFrame("Frame", nil, parent) + slot:SetWidth(SLOT_H) + slot:SetHeight(SLOT_H) + slot:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 } + }) + slot:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + slot:SetBackdropBorderColor(DEFAULT_SLOT_BORDER[1], DEFAULT_SLOT_BORDER[2], DEFAULT_SLOT_BORDER[3], DEFAULT_SLOT_BORDER[4]) + + local icon = slot:CreateTexture(nil, "ARTWORK") + icon:SetWidth(SLOT_H - 4) + icon:SetHeight(SLOT_H - 4) + icon:SetPoint("CENTER", slot, "CENTER", 0, 0) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:Hide() + slot.icon = icon + + local highlight = slot:CreateTexture(nil, "OVERLAY") + highlight:SetTexture("Interface\\Buttons\\ButtonHilight-Square") + highlight:SetBlendMode("ADD") + highlight:SetWidth(SLOT_H - 4) + highlight:SetHeight(SLOT_H - 4) + highlight:SetPoint("CENTER", slot, "CENTER", 0, 0) + highlight:SetAlpha(0) + slot.highlight = highlight + + local ilvl = slot:CreateFontString(nil, "OVERLAY") + ilvl:SetFont(GetFont(), 9, "OUTLINE") + ilvl:SetPoint("BOTTOMRIGHT", slot, "BOTTOMRIGHT", -2, 2) + ilvl:SetTextColor(1, 0.82, 0) + ilvl:SetJustifyH("RIGHT") + ilvl:Hide() + slot.ilvl = ilvl + + local count = slot:CreateFontString(nil, "OVERLAY") + count:SetFont(GetFont(), 11, "OUTLINE") + count:SetPoint("BOTTOMRIGHT", slot, "BOTTOMRIGHT", -2, 2) + count:SetTextColor(1, 1, 1) + count:SetJustifyH("RIGHT") + count:Hide() + slot.count = count + + local qualGlow = slot:CreateTexture(nil, "OVERLAY") + qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + qualGlow:SetBlendMode("ADD") + qualGlow:SetAlpha(0.8) + qualGlow:SetWidth(SLOT_H * 1.8) + qualGlow:SetHeight(SLOT_H * 1.8) + qualGlow:SetPoint("CENTER", slot, "CENTER", 0, 0) + qualGlow:Hide() + slot.qualGlow = qualGlow + + return slot + end + + local function StyleTradeItem(itemPrefix, index) + local itemFrame = _G[itemPrefix .. index] + local itemBtn = _G[itemPrefix .. index .. "ItemButton"] + local itemName = _G[itemPrefix .. index .. "Name"] + local bgSlot = _G[itemPrefix .. index .. "ItemButtonSlotTexture"] + local bgSlot2 = _G[itemPrefix .. index .. "SlotTexture"] + local bgSlot3 = _G[itemPrefix .. index .. "ItemButtonBackground"] + local nameFrame = _G[itemPrefix .. index .. "NameFrame"] + + if bgSlot then + bgSlot:SetTexture(nil); bgSlot:Hide() + bgSlot.Show = function() end + end + if bgSlot2 then + bgSlot2:SetTexture(nil); bgSlot2:Hide() + bgSlot2.Show = function() end + end + if bgSlot3 then + bgSlot3:SetTexture(nil); bgSlot3:Hide() + bgSlot3.Show = function() end + end + if nameFrame then + nameFrame:SetTexture(nil); nameFrame:Hide() + nameFrame.Show = function() end + end + + itemFrame:SetWidth(SLOT_W) + itemFrame:SetHeight(SLOT_H) + + -- Row background (create once) + if not itemFrame.sfRowBg then + local slotBg = itemFrame:CreateTexture(nil, "BACKGROUND") + slotBg:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background") + slotBg:SetVertexColor(0.08, 0.04, 0.06, 0.4) + slotBg:SetAllPoints(itemFrame) + itemFrame.sfRowBg = slotBg + end + + -- Create our pure custom slot (once) + if not itemFrame.sfSlot then + itemFrame.sfSlot = CreateSfSlot(itemFrame) + end + local sfSlot = itemFrame.sfSlot + sfSlot:ClearAllPoints() + sfSlot:SetPoint("LEFT", itemFrame, "LEFT", 1, 0) + sfSlot:SetFrameLevel(itemFrame:GetFrameLevel() + 1) + sfSlot:Show() + + -- Make Blizzard button completely invisible + HideBlizzardButton(itemBtn) + + -- Position invisible Blizzard button exactly over sfSlot for click/drag + itemBtn:ClearAllPoints() + itemBtn:SetWidth(SLOT_H) + itemBtn:SetHeight(SLOT_H) + itemBtn:SetPoint("CENTER", sfSlot, "CENTER", 0, 0) + itemBtn:SetFrameLevel(sfSlot:GetFrameLevel() + 2) + + -- Hover glow: show/hide highlight on sfSlot when mouse enters Blizzard button + if not itemBtn.sfHoverHooked then + itemBtn.sfHoverHooked = true + local origEnter = itemBtn:GetScript("OnEnter") + local origLeave = itemBtn:GetScript("OnLeave") + itemBtn:SetScript("OnEnter", function() + if origEnter then origEnter() end + local sf = this:GetParent() and this:GetParent().sfSlot + if sf and sf.highlight then sf.highlight:SetAlpha(0.35) end + end) + itemBtn:SetScript("OnLeave", function() + if origLeave then origLeave() end + local sf = this:GetParent() and this:GetParent().sfSlot + if sf and sf.highlight then sf.highlight:SetAlpha(0) end + end) + end + + -- Name text anchored to sfSlot + itemName:ClearAllPoints() + itemName:SetPoint("LEFT", sfSlot, "RIGHT", 6, 0) + itemName:SetPoint("RIGHT", itemFrame, "RIGHT", -4, 0) + itemName:SetJustifyH("LEFT") + itemName:SetFont(GetFont(), 11, "OUTLINE") + end + + -- Layout items in two columns + for i = 1, 7 do + StyleTradeItem("TradePlayerItem", i) + StyleTradeItem("TradeRecipientItem", i) + + local pf = _G["TradePlayerItem" .. i] + local rf = _G["TradeRecipientItem" .. i] + pf:ClearAllPoints() + rf:ClearAllPoints() + + if i == 1 then + pf:SetPoint("TOPLEFT", TradeFrame, "TOPLEFT", SIDE_PAD, -(HEADER_H + 8)) + rf:SetPoint("TOPRIGHT", TradeFrame, "TOPRIGHT", -SIDE_PAD, -(HEADER_H + 8)) + elseif i == 7 then + pf:SetPoint("TOPLEFT", _G["TradePlayerItem6"], "BOTTOMLEFT", 0, -NOT_TRADED_GAP) + rf:SetPoint("TOPLEFT", _G["TradeRecipientItem6"], "BOTTOMLEFT", 0, -NOT_TRADED_GAP) + + if TradeFrame.sfBottomTexts then + for idx, lbl in ipairs(TradeFrame.sfBottomTexts) do + lbl:ClearAllPoints() + lbl:SetFont(GetFont(), 10, "OUTLINE") + lbl:SetTextColor(T.labelDim[1], T.labelDim[2], T.labelDim[3]) + if idx == 1 then + lbl:SetPoint("BOTTOMLEFT", pf, "TOPLEFT", 0, 5) + else + lbl:SetPoint("BOTTOMLEFT", rf, "TOPLEFT", 0, 5) + end + end + end + else + pf:SetPoint("TOPLEFT", _G["TradePlayerItem" .. (i - 1)], "BOTTOMLEFT", 0, -SLOT_GAP) + rf:SetPoint("TOPLEFT", _G["TradeRecipientItem" .. (i - 1)], "BOTTOMLEFT", 0, -SLOT_GAP) + end + end + + -- Position status bars below item 7 + if TradeFrame.sfPlayerStatusBar then + TradeFrame.sfPlayerStatusBar:ClearAllPoints() + TradeFrame.sfPlayerStatusBar:SetPoint("TOPLEFT", _G["TradePlayerItem7"], "BOTTOMLEFT", 0, -4) + end + if TradeFrame.sfTargetStatusBar then + TradeFrame.sfTargetStatusBar:ClearAllPoints() + TradeFrame.sfTargetStatusBar:SetPoint("TOPLEFT", _G["TradeRecipientItem7"], "BOTTOMLEFT", 0, -4) + end + + -- Thin separator in the gap between slot 6 and not-traded label + local function MakeThinSep(anchor, w) + local sep = TradeFrame:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8X8") + sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + sep:SetHeight(1) + sep:SetWidth(w) + sep:SetPoint("BOTTOMLEFT", anchor, "BOTTOMLEFT", 0, -(NOT_TRADED_GAP / 2.5)) + end + MakeThinSep(_G["TradePlayerItem6"], SLOT_W) + MakeThinSep(_G["TradeRecipientItem6"], SLOT_W) + + ---------------------------------------------------------------------------- + -- Hide Blizzard confirm highlights (we handle this ourselves) + ---------------------------------------------------------------------------- + local function KillBlizzardHighlight(name) + local obj = _G[name] + if not obj then return end + if obj:IsObjectType("Texture") then + obj:SetTexture(nil); obj:SetAlpha(0); obj:Hide() + obj.Show = function() end + elseif obj:IsObjectType("Frame") then + local regs = { obj:GetRegions() } + for _, r in ipairs(regs) do + if r:IsObjectType("Texture") then r:SetTexture(nil); r:Hide() end + end + obj:SetAlpha(0); obj:Hide() + obj.Show = function() end + end + end + KillBlizzardHighlight("TradeHighlightPlayer") + KillBlizzardHighlight("TradeHighlightRecipient") + KillBlizzardHighlight("TradeHighlightPlayerEnchant") + KillBlizzardHighlight("TradeHighlightRecipientEnchant") + + ---------------------------------------------------------------------------- + -- Custom confirm overlay (our own, with proper FrameLevel + pulse) + ---------------------------------------------------------------------------- + local function CreateConfirmOverlay(parent, item1, item2) + local overlay = CreateFrame("Frame", nil, parent) + overlay:SetFrameLevel(parent:GetFrameLevel() + 8) + overlay:SetPoint("TOPLEFT", item1, "TOPLEFT", -4, 4) + overlay:SetPoint("BOTTOMRIGHT", item2, "BOTTOMRIGHT", 4, -4) + + SetRoundBackdrop(overlay, { 0, 0, 0, 0 }, T.confirmBorder) + + overlay.sfElapsed = 0 + overlay:SetScript("OnUpdate", function() + this.sfElapsed = (this.sfElapsed or 0) + arg1 + local pulse = 0.6 + 0.4 * math.sin(this.sfElapsed * 3) + this:SetBackdropBorderColor( + T.confirmBorder[1], T.confirmBorder[2], T.confirmBorder[3], pulse) + end) + + overlay:EnableMouse(false) + overlay:Hide() + return overlay + end + + TradeFrame.sfPlayerOverlay = CreateConfirmOverlay(TradeFrame, + _G["TradePlayerItem1"], _G["TradePlayerItem6"]) + TradeFrame.sfPlayerOverlayEnchant = CreateConfirmOverlay(TradeFrame, + _G["TradePlayerItem7"], _G["TradePlayerItem7"]) + TradeFrame.sfTargetOverlay = CreateConfirmOverlay(TradeFrame, + _G["TradeRecipientItem1"], _G["TradeRecipientItem6"]) + TradeFrame.sfTargetOverlayEnchant = CreateConfirmOverlay(TradeFrame, + _G["TradeRecipientItem7"], _G["TradeRecipientItem7"]) + + ---------------------------------------------------------------------------- + -- Buttons - Nanami-UI GameMenu style + ---------------------------------------------------------------------------- + local function SkinBtn(btn, bgCol, borderCol, textCol, label) + if not btn then return end + btn:SetWidth(72) + btn:SetHeight(26) + + local nt = btn:GetNormalTexture() + if nt then nt:SetTexture(nil) end + local pt = btn:GetPushedTexture() + if pt then pt:SetTexture(nil) end + local ht = btn:GetHighlightTexture() + if ht then ht:SetTexture(nil) end + local dt = btn:GetDisabledTexture() + if dt then dt:SetTexture(nil) end + + SetRoundBackdrop(btn, bgCol, borderCol) + + local origEnter = btn:GetScript("OnEnter") + local origLeave = btn:GetScript("OnLeave") + + if not btn.sfHoverHooked then + btn.sfHoverHooked = true + btn.sfBgCol = bgCol + btn.sfBorderCol = borderCol + + btn:SetScript("OnEnter", function() + if origEnter then origEnter() end + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBorder[1], T.btnHoverBorder[2], T.btnHoverBorder[3], T.btnHoverBorder[4]) + local fs = this:GetFontString() + if fs then fs:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end + end) + + btn:SetScript("OnLeave", function() + if origLeave then origLeave() end + local bg = this.sfBgCol or T.btnBg + local bd = this.sfBorderCol or T.btnBorder + this:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) + this:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) + local fs = this:GetFontString() + if fs and this.sfTextCol then fs:SetTextColor(unpack(this.sfTextCol)) end + end) + end + + local fs = btn:GetFontString() + if fs then + fs:SetFont(GetFont(), 11, "OUTLINE") + fs:SetTextColor(unpack(textCol or T.btnText)) + if label then fs:SetText(label) end + end + btn.sfTextCol = textCol or T.btnText + end + + SkinBtn(TradeFrameTradeButton, T.tradeBg, T.tradeBorder, T.tradeText, "交易") + SkinBtn(TradeFrameCancelButton, T.btnBg, T.btnBorder, T.btnText, "取消") + + TradeFrameTradeButton:ClearAllPoints() + TradeFrameCancelButton:ClearAllPoints() + TradeFrameCancelButton:SetPoint("BOTTOMRIGHT", TradeFrame, "BOTTOMRIGHT", -SIDE_PAD, 14) + TradeFrameTradeButton:SetPoint("RIGHT", TradeFrameCancelButton, "LEFT", -6, 0) + + ---------------------------------------------------------------------------- + -- Whisper checkbox + channel dropdown + ---------------------------------------------------------------------------- + local cbObj = _G["SFramesTradeWhisperObj"] + if not cbObj then + cbObj = CreateFrame("CheckButton", "SFramesTradeWhisperObj", TradeFrame, "UICheckButtonTemplate") + cbObj:SetWidth(20) + cbObj:SetHeight(20) + cbObj:SetPoint("BOTTOMLEFT", TradeFrame, "BOTTOMLEFT", SIDE_PAD, 14) + + local cbText = _G[cbObj:GetName() .. "Text"] + if cbText then + cbText:SetFont(GetFont(), 11, "OUTLINE") + cbText:SetText(L.WHISPER_CHECK) + cbText:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + end + + if SFramesDB.TradeWhisperEnable == nil then SFramesDB.TradeWhisperEnable = false end + cbObj:SetChecked(SFramesDB.TradeWhisperEnable and 1 or 0) + cbObj:SetScript("OnClick", function() + SFramesDB = SFramesDB or {} + SFramesDB.TradeWhisperEnable = (this:GetChecked() == 1) + end) + + local drop = CreateFrame("Frame", "SFramesTradeChannelObj", TradeFrame, "UIDropDownMenuTemplate") + drop:SetPoint("LEFT", cbText or cbObj, "RIGHT", -8, -1) + UIDropDownMenu_SetWidth(60, drop) + + local dropText = _G[drop:GetName() .. "Text"] + if dropText then + dropText:SetFont(GetFont(), 10, "OUTLINE") + dropText:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + end + + local channels = { + { text = "密语", value = "WHISPER" }, + { text = "小队", value = "PARTY" }, + { text = "当前", value = "SAY" }, + } + + local function TradeDropDownInit() + SFramesDB = SFramesDB or {} + local selected = SFramesDB.TradeWhisperChannel or "WHISPER" + for _, info in ipairs(channels) do + local d = {} + d.text = info.text + d.value = info.value + d.func = function() + SFramesDB = SFramesDB or {} + SFramesDB.TradeWhisperChannel = this.value + UIDropDownMenu_SetSelectedValue(drop, this.value) + local txt = _G[drop:GetName() .. "Text"] + if txt then txt:SetText(info.text) end + end + d.checked = (info.value == selected) + UIDropDownMenu_AddButton(d) + end + end + + UIDropDownMenu_Initialize(drop, TradeDropDownInit) + SFramesDB = SFramesDB or {} + UIDropDownMenu_SetSelectedValue(drop, SFramesDB.TradeWhisperChannel or "WHISPER") + if dropText then + for _, info in ipairs(channels) do + if info.value == (SFramesDB.TradeWhisperChannel or "WHISPER") then + dropText:SetText(info.text) + break + end + end + end + end + + -- Close button + local closeBtn = _G["TradeFrameCloseButton"] + if closeBtn then + closeBtn:ClearAllPoints() + closeBtn:SetPoint("TOPRIGHT", TradeFrame, "TOPRIGHT", -2, -2) + end + + HookTradeItemRightClick() +end + +-------------------------------------------------------------------------------- +-- Hooks +-------------------------------------------------------------------------------- +local Hook_TradeFrame_OnShow = TradeFrame_OnShow +function TradeFrame_OnShow() + if Hook_TradeFrame_OnShow then Hook_TradeFrame_OnShow() end + SkinTradeFrame() + ForceRefreshTradeVisuals() +end + +local function UpdateSfSlot(sfSlot, texture, numItems, link, ilvl, quality) + if not sfSlot then return end + + -- Icon texture + if texture then + sfSlot.icon:SetTexture(texture) + sfSlot.icon:Show() + else + sfSlot.icon:SetTexture(nil) + sfSlot.icon:Hide() + end + + -- Stack count + if numItems and numItems > 1 then + sfSlot.count:SetText(numItems) + sfSlot.count:Show() + sfSlot.ilvl:Hide() + else + sfSlot.count:Hide() + -- Item level (only show if no stack count) + if ilvl and ilvl > 0 and texture then + sfSlot.ilvl:SetText(ilvl) + sfSlot.ilvl:Show() + else + sfSlot.ilvl:Hide() + end + end + + -- Quality border + local r, g, b = nil, nil, nil + if link and not IsCommonOrPoor(link) then + r, g, b = GetQualityColorFromLink(link) + end + if (not r) and type(quality) == "number" and quality ~= 1 then + r, g, b = GetQualityColorFromRarity(quality) + end + if r then + if sfSlot.qualGlow then + sfSlot.qualGlow:SetVertexColor(r, g, b) + sfSlot.qualGlow:Show() + end + else + if sfSlot.qualGlow then + sfSlot.qualGlow:Hide() + end + end +end + +local function UpdateSlotNameColor(nameObj, link, quality) + if not nameObj then return end + local r, g, b = nil, nil, nil + if link then + r, g, b = GetQualityColorFromLink(link) + end + if (not r) and type(quality) == "number" and quality ~= 1 then + r, g, b = GetQualityColorFromRarity(quality) + end + if r then + nameObj:SetTextColor(r, g, b) + return + end + nameObj:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) +end + +local function SetStatusBarState(bar, confirmed) + if not bar then return end + if confirmed then + bar.sfConfirmed = true + bar.sfElapsed = 0 + SetRoundBackdrop(bar, T.statusConfirmBg, T.statusConfirmBd) + if bar.sfGlow then + bar.sfGlow:SetVertexColor(T.confirmOverlay[1], T.confirmOverlay[2], T.confirmOverlay[3], 1) + bar.sfGlow:SetAlpha(0.35) + end + if bar.sfIcon then + bar.sfIcon:SetText("|cff33ff55>>|r") + bar.sfIcon:SetTextColor(0.3, 1, 0.4) + end + if bar.sfLabel then + bar.sfLabel:SetText(L.CONFIRMED) + bar.sfLabel:SetTextColor(T.confirmText[1], T.confirmText[2], T.confirmText[3]) + end + else + bar.sfConfirmed = false + SetRoundBackdrop(bar, T.statusWaitBg, T.statusWaitBd) + if bar.sfGlow then bar.sfGlow:SetAlpha(0) end + if bar.sfIcon then + bar.sfIcon:SetText("...") + bar.sfIcon:SetTextColor(T.statusWaitText[1], T.statusWaitText[2], T.statusWaitText[3]) + end + if bar.sfLabel then + bar.sfLabel:SetText(L.WAITING) + bar.sfLabel:SetTextColor(T.statusWaitText[1], T.statusWaitText[2], T.statusWaitText[3]) + end + end + bar:Show() +end + +local function UpdateConfirmStatus() + if not TradeFrame then return end + + local playerConfirmed = TRADE_DATA.playerAccepted + local targetConfirmed = TRADE_DATA.targetAccepted + + -- Status bars + SetStatusBarState(TradeFrame.sfPlayerStatusBar, playerConfirmed) + SetStatusBarState(TradeFrame.sfTargetStatusBar, targetConfirmed) + + -- Green overlays on item columns + if TradeFrame.sfPlayerOverlay then + if playerConfirmed then TradeFrame.sfPlayerOverlay:Show() + else TradeFrame.sfPlayerOverlay:Hide() end + end + if TradeFrame.sfPlayerOverlayEnchant then + if playerConfirmed then TradeFrame.sfPlayerOverlayEnchant:Show() + else TradeFrame.sfPlayerOverlayEnchant:Hide() end + end + if TradeFrame.sfTargetOverlay then + if targetConfirmed then TradeFrame.sfTargetOverlay:Show() + else TradeFrame.sfTargetOverlay:Hide() end + end + if TradeFrame.sfTargetOverlayEnchant then + if targetConfirmed then TradeFrame.sfTargetOverlayEnchant:Show() + else TradeFrame.sfTargetOverlayEnchant:Hide() end + end +end + +local Hook_TradeFrame_Update = TradeFrame_Update +function TradeFrame_Update() + if Hook_TradeFrame_Update then Hook_TradeFrame_Update() end + + for i = 1, 7 do + -- Re-hide Blizzard visuals (Blizzard code re-shows them every update) + ReHideBlizzardSlot("TradePlayerItem", i) + ReHideBlizzardSlot("TradeRecipientItem", i) + + -- Read from Blizzard API + local pName, pTex, pNum, pQuality = GetTradePlayerItemInfo(i) + local rName, rTex, rNum, rQuality = GetTradeTargetItemInfo(i) + local pLink = GetTradePlayerItemLink(i) + local rLink = GetTradeTargetItemLink and GetTradeTargetItemLink(i) + + if (not pTex or pTex == "") then + local pIconObj = _G["TradePlayerItem" .. i .. "ItemButtonIconTexture"] + if pIconObj and pIconObj.GetTexture then + pTex = pIconObj:GetTexture() + end + end + if (not rTex or rTex == "") then + local rIconObj = _G["TradeRecipientItem" .. i .. "ItemButtonIconTexture"] + if rIconObj and rIconObj.GetTexture then + rTex = rIconObj:GetTexture() + end + end + if (not pTex or pTex == "") and pLink and GetItemIcon then + pTex = GetItemIcon(pLink) + end + if (not rTex or rTex == "") and rLink and GetItemIcon then + rTex = GetItemIcon(rLink) + end + if (not pTex or pTex == "") and pName and GetItemInfo then + local _, _, _, _, _, _, _, _, pInfoTex = GetItemInfo(pName) + pTex = pInfoTex or pTex + end + if (not rTex or rTex == "") and rName and GetItemInfo then + local _, _, _, _, _, _, _, _, rInfoTex = GetItemInfo(rName) + rTex = rInfoTex or rTex + end + + pQuality = ResolveTradeItemQuality(pLink, pQuality, pName) + rQuality = ResolveTradeItemQuality(rLink, rQuality, rName) + + local pIlvl = (pTex or pName or pLink) and GetTradePlayerItemLevel(i) or nil + local rIlvl = (rTex or rName or rLink) and GetTradeTargetItemLevel(i) or nil + + -- Write to custom sfSlot + local pFrame = _G["TradePlayerItem" .. i] + local rFrame = _G["TradeRecipientItem" .. i] + if pFrame then UpdateSfSlot(pFrame.sfSlot, pTex, pNum, pLink, pIlvl, pQuality) end + if rFrame then UpdateSfSlot(rFrame.sfSlot, rTex, rNum, rLink, rIlvl, rQuality) end + + UpdateSlotNameColor(_G["TradePlayerItem" .. i .. "Name"], pLink, pQuality) + UpdateSlotNameColor(_G["TradeRecipientItem" .. i .. "Name"], rLink, rQuality) + end + + UpdateConfirmStatus() +end + +local Hook_MoneyFrame_Update = MoneyFrame_Update +function MoneyFrame_Update(frameName, money) + if Hook_MoneyFrame_Update then Hook_MoneyFrame_Update(frameName, money) end +end + diff --git a/TradeSkillDB.lua b/TradeSkillDB.lua new file mode 100644 index 0000000..2ee417a --- /dev/null +++ b/TradeSkillDB.lua @@ -0,0 +1,9852 @@ +-- Auto-generated from LibCrafts-1.0 + AtlasLoot thresholds +-- Keys: itemID (number), Chinese name (string), English name (string) +-- Lazy-loaded: call NanamiTradeSkillDB_Init() before first use +function NanamiTradeSkillDB_Init() + if NanamiTradeSkillDB then return end +NanamiTradeSkillDB = { +[65]={175,175,182,190}, +[66]={175,175,180,185}, +[67]={235,235,240,245}, +[82]={275,275,280,285}, +[87]={285,285,297,310}, +[103]={300,300,310,320}, +[118]={1,55,75,95}, +[131]={200,195,200,205}, +[151]={225,225,230,235}, +[156]={185,185,200,215}, +[724]={50,90,110,130}, +[733]={75,115,135,155}, +[787]={1,45,65,85}, +[858]={55,85,105,125}, +[929]={110,135,155,175}, +[1017]={100,140,160,180}, +[1082]={100,135,155,175}, +[1251]={1,30,45,60}, +[1710]={155,175,195,215}, +[2300]={40,70,85,100}, +[2302]={1,40,55,70}, +[2303]={15,45,60,75}, +[2304]={1,30,45,60}, +[2307]={90,120,135,150}, +[2308]={85,105,120,135}, +[2309]={55,85,100,115}, +[2310]={60,90,105,120}, +[2311]={60,90,105,120}, +[2312]={75,105,120,135}, +[2313]={100,115,122,130}, +[2314]={120,145,157,170}, +[2315]={100,125,137,150}, +[2316]={110,135,147,160}, +[2317]={100,125,137,150}, +[2318]={1,20,30,40}, +[2319]={100,100,105,110}, +[2454]={1,55,75,95}, +[2455]={25,65,85,105}, +[2456]={40,70,90,110}, +[2457]={50,80,100,120}, +[2458]={50,80,100,120}, +[2459]={60,90,110,130}, +[2568]={10,45,57,70}, +[2569]={65,90,107,125}, +[2570]={1,35,47,60}, +[2572]={40,65,82,100}, +[2575]={40,65,82,100}, +[2576]={1,35,47,60}, +[2577]={40,65,82,100}, +[2578]={70,95,112,130}, +[2579]={70,95,112,130}, +[2580]={60,85,102,120}, +[2581]={40,50,75,100}, +[2582]={85,110,127,145}, +[2583]={95,120,137,155}, +[2584]={75,100,117,135}, +[2585]={105,130,147,165}, +[2587]={100,110,120,130}, +[2679]={1,45,65,85}, +[2680]={10,50,70,90}, +[2681]={1,45,65,85}, +[2682]={85,125,145,165}, +[2683]={75,115,135,155}, +[2684]={50,90,110,130}, +[2685]={110,130,150,170}, +[2687]={80,120,140,160}, +[2840]={1,25,45,70}, +[2841]={65,65,90,115}, +[2842]={75,100,112,125}, +[2844]={15,55,75,95}, +[2845]={20,60,80,100}, +[2847]={25,65,85,105}, +[2848]={110,140,155,170}, +[2849]={115,145,160,175}, +[2850]={120,150,165,180}, +[2851]={35,75,95,115}, +[2852]={1,50,70,90}, +[2853]={1,20,40,60}, +[2854]={90,115,127,140}, +[2857]={70,110,130,150}, +[2862]={1,15,35,55}, +[2863]={65,65,72,80}, +[2864]={80,120,140,160}, +[2865]={105,145,160,175}, +[2866]={105,145,160,175}, +[2867]={100,140,160,180}, +[2868]={120,150,165,180}, +[2869]={130,160,175,190}, +[2870]={145,175,190,205}, +[2871]={125,125,132,140}, +[2888]={25,60,80,100}, +[2892]={130,175,200,225}, +[2893]={170,215,240,265}, +[2996]={1,25,37,50}, +[2997]={75,90,97,105}, +[3220]={60,100,120,140}, +[3239]={1,15,35,55}, +[3240]={65,65,72,80}, +[3241]={125,125,132,140}, +[3382]={15,60,80,100}, +[3383]={90,120,140,160}, +[3384]={110,135,155,175}, +[3385]={120,145,165,185}, +[3386]={120,145,165,185}, +[3387]={250,275,295,315}, +[3388]={125,150,170,190}, +[3389]={130,155,175,195}, +[3390]={140,165,185,205}, +[3391]={150,175,195,215}, +[3469]={20,60,80,100}, +[3470]={25,45,65,85}, +[3471]={35,75,95,115}, +[3472]={40,80,100,120}, +[3473]={45,85,105,125}, +[3474]={60,100,120,140}, +[3478]={75,75,87,100}, +[3480]={110,140,155,170}, +[3481]={125,155,170,185}, +[3482]={130,160,175,190}, +[3483]={135,165,180,195}, +[3484]={145,175,190,205}, +[3485]={150,180,195,210}, +[3486]={125,125,137,150}, +[3487]={95,135,155,175}, +[3488]={35,75,95,115}, +[3489]={70,110,130,150}, +[3490]={125,155,170,185}, +[3491]={130,160,175,190}, +[3492]={145,175,190,205}, +[3530]={80,80,115,150}, +[3531]={115,115,150,185}, +[3575]={125,125,130,140}, +[3576]={65,65,70,75}, +[3577]={225,240,260,280}, +[3662]={80,120,140,160}, +[3663]={90,130,150,170}, +[3664]={120,160,180,200}, +[3665]={130,170,190,210}, +[3666]={110,150,170,190}, +[3719]={150,170,180,190}, +[3726]={110,150,170,190}, +[3727]={125,175,195,215}, +[3728]={150,190,210,230}, +[3729]={175,215,235,255}, +[3775]={1,125,150,175}, +[3776]={230,275,300,325}, +[3823]={165,185,205,225}, +[3824]={165,190,210,230}, +[3825]={175,195,215,235}, +[3826]={180,200,220,240}, +[3827]={160,180,200,220}, +[3828]={195,215,235,255}, +[3829]={200,220,240,260}, +[3835]={165,190,202,215}, +[3836]={170,195,207,220}, +[3837]={190,215,227,240}, +[3840]={160,185,197,210}, +[3841]={175,200,212,225}, +[3842]={155,180,192,205}, +[3843]={170,195,207,220}, +[3844]={180,205,217,230}, +[3845]={195,220,232,245}, +[3846]={185,210,222,235}, +[3847]={200,225,237,250}, +[3848]={105,135,150,165}, +[3849]={160,185,197,210}, +[3850]={175,200,212,225}, +[3851]={155,180,192,205}, +[3852]={170,195,207,220}, +[3853]={180,205,217,230}, +[3854]={200,225,237,250}, +[3855]={185,210,222,235}, +[3856]={200,225,237,250}, +[3859]={165,165,165,165}, +[3860]={175,175,175,175}, +[3928]={215,230,250,270}, +[4231]={35,55,65,75}, +[4233]={100,115,122,130}, +[4234]={150,150,155,160}, +[4236]={150,160,165,170}, +[4237]={25,55,70,85}, +[4238]={45,70,87,105}, +[4239]={55,85,100,115}, +[4240]={80,105,122,140}, +[4241]={95,120,137,155}, +[4242]={75,105,120,135}, +[4243]={85,115,130,145}, +[4244]={100,125,137,150}, +[4245]={150,170,185,200}, +[4246]={80,110,125,140}, +[4247]={145,170,182,195}, +[4248]={120,155,167,180}, +[4249]={125,150,162,175}, +[4250]={120,145,157,170}, +[4251]={130,155,167,180}, +[4252]={140,165,177,190}, +[4253]={135,160,172,185}, +[4254]={150,170,180,190}, +[4255]={155,175,185,195}, +[4256]={175,195,205,215}, +[4257]={160,180,190,200}, +[4258]={170,190,200,210}, +[4259]={180,200,210,220}, +[4260]={195,215,225,235}, +[4262]={185,205,215,225}, +[4264]={200,220,230,240}, +[4265]={150,170,180,190}, +[4304]={200,200,202,205}, +[4305]={125,135,140,145}, +[4307]={35,60,77,95}, +[4308]={60,85,102,120}, +[4309]={70,95,112,130}, +[4310]={85,110,127,145}, +[4311]={100,125,142,160}, +[4312]={80,105,122,140}, +[4313]={95,120,137,155}, +[4314]={110,135,152,170}, +[4315]={120,145,162,180}, +[4316]={110,135,152,170}, +[4317]={125,150,167,185}, +[4318]={130,150,165,180}, +[4319]={145,165,180,195}, +[4320]={125,150,167,185}, +[4321]={140,160,175,190}, +[4322]={165,185,200,215}, +[4323]={170,190,205,220}, +[4324]={150,170,185,200}, +[4325]={175,195,210,225}, +[4326]={185,205,220,235}, +[4327]={200,220,235,250}, +[4328]={180,200,215,230}, +[4329]={200,220,235,250}, +[4330]={110,135,152,170}, +[4331]={125,150,167,185}, +[4332]={135,145,150,155}, +[4333]={155,165,170,175}, +[4334]={170,180,185,190}, +[4335]={185,195,200,205}, +[4336]={200,210,215,220}, +[4339]={175,180,182,185}, +[4343]={30,55,72,90}, +[4344]={1,35,47,60}, +[4357]={1,20,30,40}, +[4358]={1,30,45,60}, +[4359]={30,45,52,60}, +[4360]={30,60,75,90}, +[4361]={50,80,95,110}, +[4362]={50,80,95,110}, +[4363]={65,95,110,125}, +[4364]={75,85,90,95}, +[4365]={75,90,97,105}, +[4366]={85,115,130,145}, +[4367]={100,130,145,160}, +[4368]={100,130,145,160}, +[4369]={105,130,142,155}, +[4370]={105,105,130,155}, +[4371]={105,105,130,155}, +[4372]={120,145,157,170}, +[4373]={120,145,157,170}, +[4374]={120,120,145,170}, +[4375]={125,125,150,175}, +[4376]={125,125,150,175}, +[4377]={125,125,135,145}, +[4378]={125,125,135,145}, +[4379]={130,155,167,180}, +[4380]={140,140,165,190}, +[4381]={140,165,177,190}, +[4382]={145,145,170,195}, +[4383]={145,170,182,195}, +[4384]={150,175,187,200}, +[4385]={150,175,187,200}, +[4386]={155,175,185,195}, +[4387]={160,160,170,180}, +[4388]={160,180,190,200}, +[4389]={170,170,190,210}, +[4390]={175,175,195,215}, +[4391]={175,175,195,215}, +[4392]={185,185,205,225}, +[4393]={185,205,215,225}, +[4394]={190,190,210,230}, +[4395]={195,215,225,235}, +[4396]={200,220,230,240}, +[4397]={200,220,230,240}, +[4398]={200,200,220,240}, +[4401]={75,105,120,135}, +[4403]={165,185,195,205}, +[4404]={90,110,125,140}, +[4405]={60,90,105,120}, +[4406]={110,135,147,160}, +[4407]={180,200,210,220}, +[4455]={165,185,195,205}, +[4456]={165,185,195,205}, +[4457]={175,215,235,255}, +[4592]={50,90,110,130}, +[4593]={100,140,160,180}, +[4594]={175,190,210,230}, +[4596]={50,80,100,120}, +[4623]={215,230,250,270}, +[4852]={185,185,205,225}, +[5081]={40,70,85,100}, +[5095]={50,90,110,130}, +[5237]={1,150,175,200}, +[5472]={10,50,70,90}, +[5473]={20,60,80,100}, +[5474]={35,75,95,115}, +[5476]={50,90,110,130}, +[5477]={50,90,110,130}, +[5478]={90,130,150,170}, +[5479]={100,140,160,180}, +[5480]={110,150,170,190}, +[5507]={135,160,172,185}, +[5525]={50,90,110,130}, +[5526]={90,130,150,170}, +[5527]={125,165,185,205}, +[5530]={1,170,195,220}, +[5540]={110,140,155,170}, +[5541]={140,170,185,200}, +[5542]={90,115,132,150}, +[5631]={60,90,110,130}, +[5633]={175,195,215,235}, +[5634]={150,175,195,215}, +[5739]={190,210,220,230}, +[5762]={70,95,112,130}, +[5763]={115,140,157,175}, +[5764]={175,195,210,225}, +[5765]={185,205,220,235}, +[5766]={135,155,170,185}, +[5770]={150,170,185,200}, +[5780]={90,120,135,150}, +[5781]={95,125,140,155}, +[5782]={170,190,200,210}, +[5783]={190,210,220,230}, +[5957]={1,40,55,70}, +[5958]={105,130,142,155}, +[5961]={115,140,152,165}, +[5962]={160,180,190,200}, +[5963]={170,190,200,210}, +[5964]={175,195,205,215}, +[5965]={185,205,215,225}, +[5966]={190,210,220,230}, +[5996]={90,120,140,160}, +[5997]={1,55,75,95}, +[6037]={225,240,260,280}, +[6038]={175,215,235,255}, +[6040]={185,210,222,235}, +[6041]={190,215,227,240}, +[6042]={150,180,195,210}, +[6043]={165,190,202,215}, +[6048]={135,160,180,200}, +[6049]={165,210,230,250}, +[6050]={190,205,225,245}, +[6051]={100,130,150,170}, +[6052]={190,210,230,250}, +[6149]={205,220,240,260}, +[6214]={65,105,125,145}, +[6218]={1,5,7,10}, +[6219]={50,70,80,90}, +[6238]={30,55,72,90}, +[6239]={55,80,97,115}, +[6240]={55,80,97,115}, +[6241]={30,55,72,90}, +[6242]={70,95,112,130}, +[6243]={90,0,0,0}, +[6263]={100,125,142,160}, +[6264]={115,140,157,175}, +[6290]={1,45,65,85}, +[6316]={50,90,110,130}, +[6338]={100,105,107,110}, +[6339]={100,130,150,170}, +[6350]={95,125,140,155}, +[6370]={80,80,90,100}, +[6371]={130,150,160,170}, +[6372]={100,130,150,170}, +[6373]={140,165,185,205}, +[6384]={120,145,162,180}, +[6385]={120,145,162,180}, +[6450]={150,150,180,210}, +[6451]={180,180,210,240}, +[6452]={80,80,115,150}, +[6453]={130,130,165,200}, +[6466]={90,120,135,150}, +[6467]={105,130,142,155}, +[6468]={115,140,152,165}, +[6533]={150,150,160,170}, +[6657]={85,125,145,165}, +[6662]={90,120,140,160}, +[6709]={90,115,130,145}, +[6712]={100,115,122,130}, +[6714]={100,115,122,130}, +[6730]={70,110,130,150}, +[6731]={100,140,160,180}, +[6733]={140,0,0,0}, +[6786]={40,65,82,100}, +[6787]={110,135,152,170}, +[6795]={160,170,175,180}, +[6796]={175,185,190,195}, +[6887]={225,265,285,305}, +[6888]={1,45,65,85}, +[6890]={40,80,100,120}, +[6947]={1,125,150,175}, +[6949]={120,165,190,215}, +[6950]={160,205,230,255}, +[6951]={1,215,240,265}, +[7026]={15,50,67,85}, +[7027]={140,160,175,190}, +[7046]={140,160,175,190}, +[7047]={145,165,180,195}, +[7048]={145,155,160,165}, +[7049]={150,170,185,200}, +[7050]={160,170,175,180}, +[7051]={170,190,205,220}, +[7052]={175,195,210,225}, +[7053]={175,195,210,225}, +[7054]={190,210,225,240}, +[7055]={175,195,210,225}, +[7056]={180,200,215,230}, +[7057]={180,200,215,230}, +[7058]={185,205,215,225}, +[7059]={190,210,225,240}, +[7060]={190,210,225,240}, +[7061]={195,215,230,245}, +[7062]={195,215,225,235}, +[7063]={205,220,235,250}, +[7064]={210,225,240,255}, +[7065]={165,185,200,215}, +[7067]={300,315,322,330}, +[7068]={300,301,305,310}, +[7070]={300,315,322,330}, +[7071]={150,150,152,155}, +[7076]={275,275,282,290}, +[7078]={275,275,282,290}, +[7080]={275,275,282,290}, +[7082]={275,275,282,290}, +[7148]={165,165,180,200}, +[7166]={30,70,90,110}, +[7189]={225,245,255,265}, +[7276]={1,40,55,70}, +[7277]={1,40,55,70}, +[7278]={30,60,75,90}, +[7279]={30,60,75,90}, +[7280]={35,65,80,95}, +[7281]={70,100,115,130}, +[7282]={95,125,140,155}, +[7283]={100,125,137,150}, +[7284]={120,145,157,170}, +[7285]={120,145,157,170}, +[7348]={125,150,162,175}, +[7349]={135,160,172,185}, +[7352]={135,160,172,185}, +[7358]={140,165,177,190}, +[7359]={145,170,182,195}, +[7371]={150,170,180,190}, +[7372]={150,170,180,190}, +[7373]={165,185,195,205}, +[7374]={175,195,205,215}, +[7375]={175,195,205,215}, +[7377]={180,200,210,220}, +[7378]={185,205,215,225}, +[7386]={190,210,220,230}, +[7387]={195,215,225,235}, +[7390]={200,220,230,240}, +[7391]={200,220,230,240}, +[7506]={125,150,162,175}, +[7676]={60,100,120,140}, +[7913]={160,185,197,210}, +[7914]={160,185,197,210}, +[7915]={175,200,212,225}, +[7916]={180,205,217,230}, +[7917]={185,210,222,235}, +[7918]={205,225,235,245}, +[7919]={205,225,235,245}, +[7920]={210,230,240,250}, +[7921]={210,230,240,250}, +[7922]={215,235,245,255}, +[7924]={215,235,245,255}, +[7925]={220,240,250,260}, +[7926]={220,240,250,260}, +[7927]={220,240,250,260}, +[7928]={225,245,255,265}, +[7929]={210,250,260,270}, +[7930]={230,250,260,270}, +[7931]={230,250,260,270}, +[7932]={235,255,265,275}, +[7933]={235,255,265,275}, +[7934]={245,255,265,275}, +[7935]={210,260,270,280}, +[7936]={210,265,275,285}, +[7937]={210,265,275,285}, +[7938]={225,245,255,265}, +[7939]={245,265,275,285}, +[7941]={210,235,247,260}, +[7942]={220,245,257,270}, +[7943]={225,250,262,275}, +[7944]={240,265,277,290}, +[7945]={230,255,267,280}, +[7946]={245,270,282,295}, +[7947]={255,280,292,305}, +[7954]={235,260,272,285}, +[7955]={30,70,90,110}, +[7956]={125,155,170,185}, +[7957]={130,160,175,190}, +[7958]={135,165,180,195}, +[7959]={250,275,287,300}, +[7960]={260,285,297,310}, +[7961]={245,270,282,295}, +[7963]={200,225,237,250}, +[7964]={200,200,205,210}, +[7965]={200,200,205,210}, +[7966]={200,200,205,210}, +[7967]={215,235,245,255}, +[7969]={235,255,265,275}, +[8067]={1,30,45,60}, +[8068]={75,85,90,95}, +[8069]={125,125,135,145}, +[8170]={250,250,250,250}, +[8172]={200,200,200,200}, +[8173]={200,220,230,240}, +[8174]={200,220,230,240}, +[8175]={205,225,235,245}, +[8176]={205,225,235,245}, +[8185]={235,255,265,275}, +[8187]={205,225,235,245}, +[8189]={210,230,240,250}, +[8191]={230,250,260,270}, +[8192]={210,230,240,250}, +[8193]={230,250,260,270}, +[8195]={230,250,260,270}, +[8197]={235,255,265,275}, +[8198]={210,230,240,250}, +[8200]={215,235,245,255}, +[8201]={220,240,250,260}, +[8202]={240,260,270,280}, +[8203]={220,240,250,260}, +[8204]={225,245,255,265}, +[8205]={220,240,250,260}, +[8206]={245,265,275,285}, +[8207]={240,260,270,280}, +[8208]={250,270,280,290}, +[8209]={235,255,265,275}, +[8210]={220,240,250,260}, +[8211]={225,245,255,265}, +[8212]={250,270,280,290}, +[8213]={245,265,275,285}, +[8214]={225,245,255,265}, +[8215]={250,270,280,290}, +[8216]={240,260,270,280}, +[8217]={225,245,255,265}, +[8218]={225,245,255,265}, +[8345]={225,245,255,265}, +[8346]={230,250,260,270}, +[8347]={225,245,255,265}, +[8348]={250,270,280,290}, +[8349]={250,270,280,290}, +[8364]={175,215,235,255}, +[8367]={255,275,285,295}, +[8544]={210,210,240,270}, +[8545]={240,240,270,300}, +[8546]={300,300,330,360}, +[8926]={200,245,270,295}, +[8927]={240,285,340,335}, +[8928]={280,325,350,375}, +[8949]={185,205,225,245}, +[8951]={195,215,235,255}, +[8956]={205,220,240,260}, +[8984]={210,255,280,305}, +[8985]={250,295,320,345}, +[9030]={210,225,245,265}, +[9036]={210,225,245,265}, +[9060]={200,225,237,250}, +[9061]={210,225,245,265}, +[9088]={240,255,275,295}, +[9144]={225,240,260,280}, +[9149]={200,240,260,280}, +[9154]={230,245,265,285}, +[9155]={235,250,270,290}, +[9172]={235,250,270,290}, +[9179]={235,250,270,290}, +[9186]={1,285,310,335}, +[9187]={240,255,275,295}, +[9197]={240,255,275,295}, +[9206]={245,260,280,300}, +[9210]={245,260,280,300}, +[9224]={250,265,285,305}, +[9233]={250,265,285,305}, +[9264]={250,265,285,305}, +[9312]={150,150,162,175}, +[9313]={150,150,162,175}, +[9318]={150,150,162,175}, +[9366]={205,225,235,245}, +[9998]={205,220,235,250}, +[9999]={205,220,235,250}, +[10001]={210,225,240,255}, +[10002]={210,225,240,255}, +[10003]={215,230,245,260}, +[10004]={215,230,245,260}, +[10007]={215,230,245,260}, +[10008]={215,220,225,230}, +[10009]={215,230,245,260}, +[10010]={220,235,250,265}, +[10011]={220,235,250,265}, +[10018]={225,240,255,270}, +[10019]={225,240,255,270}, +[10020]={225,240,255,270}, +[10021]={225,240,255,270}, +[10023]={225,240,255,270}, +[10024]={230,245,260,275}, +[10025]={245,260,275,290}, +[10026]={230,245,260,275}, +[10027]={230,245,260,275}, +[10028]={235,250,265,280}, +[10029]={235,250,265,280}, +[10030]={240,255,270,285}, +[10031]={240,255,270,285}, +[10032]={240,255,270,285}, +[10033]={240,255,270,285}, +[10034]={240,245,250,255}, +[10035]={245,250,255,260}, +[10036]={250,265,280,295}, +[10038]={245,260,275,290}, +[10039]={250,265,280,295}, +[10040]={250,255,260,265}, +[10041]={250,265,280,295}, +[10042]={225,240,255,270}, +[10044]={245,260,275,290}, +[10045]={1,35,47,60}, +[10046]={20,50,67,85}, +[10047]={75,100,117,135}, +[10048]={120,145,162,180}, +[10050]={225,240,255,270}, +[10051]={235,250,265,280}, +[10052]={220,225,230,235}, +[10053]={235,240,245,250}, +[10054]={230,235,240,245}, +[10055]={235,240,245,250}, +[10056]={215,220,225,230}, +[10421]={1,15,35,55}, +[10423]={155,180,192,205}, +[10498]={175,175,195,215}, +[10499]={175,195,205,215}, +[10500]={205,225,235,245}, +[10501]={220,240,250,260}, +[10502]={225,245,255,265}, +[10503]={230,250,260,270}, +[10504]={245,265,275,285}, +[10505]={175,175,185,195}, +[10506]={230,250,260,270}, +[10507]={175,175,185,195}, +[10508]={205,225,235,245}, +[10510]={220,240,250,260}, +[10512]={210,210,230,250}, +[10513]={245,245,265,285}, +[10514]={215,215,235,255}, +[10518]={225,245,255,265}, +[10542]={205,225,235,245}, +[10543]={205,225,235,245}, +[10545]={210,230,240,250}, +[10546]={210,230,240,250}, +[10548]={240,260,270,280}, +[10558]={150,150,170,190}, +[10559]={195,195,215,235}, +[10560]={200,200,220,240}, +[10561]={215,215,235,255}, +[10562]={235,235,255,275}, +[10576]={250,270,280,290}, +[10577]={205,205,205,205}, +[10585]={225,240,250,260}, +[10586]={235,235,255,275}, +[10587]={230,230,250,270}, +[10588]={245,265,275,285}, +[10592]={200,220,240,260}, +[10644]={205,205,205,205}, +[10645]={240,260,270,280}, +[10646]={205,205,225,245}, +[10713]={205,205,205,205}, +[10716]={205,225,235,245}, +[10720]={210,230,240,250}, +[10721]={215,235,245,255}, +[10724]={225,245,255,265}, +[10725]={230,250,260,270}, +[10726]={235,255,265,275}, +[10727]={240,260,270,280}, +[10841]={175,175,190,205}, +[10918]={1,185,210,235}, +[10920]={180,0,0,0}, +[10921]={220,0,0,0}, +[10922]={260,0,0,0}, +[11128]={150,155,157,160}, +[11130]={150,175,195,215}, +[11144]={200,205,207,210}, +[11145]={200,220,240,260}, +[11287]={10,75,95,115}, +[11288]={70,110,130,150}, +[11289]={155,175,195,215}, +[11290]={175,195,215,235}, +[11371]={230,230,230,230}, +[11590]={200,200,220,240}, +[11604]={285,305,315,325}, +[11605]={280,300,310,320}, +[11606]={270,290,300,310}, +[11607]={275,295,305,315}, +[11608]={265,285,295,305}, +[11811]={265,285,305,325}, +[11825]={205,205,205,205}, +[11826]={205,205,205,205}, +[12190]={230,245,265,285}, +[12209]={125,165,185,205}, +[12210]={175,215,235,255}, +[12212]={175,215,235,255}, +[12213]={175,215,235,255}, +[12214]={175,215,235,255}, +[12215]={200,240,260,280}, +[12216]={225,265,285,305}, +[12217]={200,240,260,280}, +[12218]={225,265,285,305}, +[12224]={1,45,65,85}, +[12259]={180,205,217,230}, +[12260]={190,215,227,240}, +[12359]={250,250,250,250}, +[12360]={275,275,282,290}, +[12404]={250,255,257,260}, +[12405]={250,270,280,290}, +[12406]={250,270,280,290}, +[12408]={255,275,285,295}, +[12409]={280,300,310,320}, +[12410]={280,300,310,320}, +[12414]={300,320,330,340}, +[12415]={270,290,300,310}, +[12416]={260,280,290,300}, +[12417]={295,315,325,335}, +[12418]={285,305,315,325}, +[12419]={290,310,320,330}, +[12420]={300,320,330,340}, +[12422]={300,320,330,340}, +[12424]={265,285,295,305}, +[12425]={270,290,300,310}, +[12426]={295,315,325,335}, +[12427]={295,315,325,335}, +[12428]={265,285,295,305}, +[12429]={300,320,330,340}, +[12610]={300,320,330,340}, +[12611]={300,320,330,340}, +[12612]={300,320,330,340}, +[12613]={300,320,330,340}, +[12614]={300,320,330,340}, +[12618]={300,320,330,340}, +[12619]={300,320,330,340}, +[12620]={300,320,330,340}, +[12624]={270,290,300,310}, +[12625]={290,310,320,330}, +[12628]={285,305,315,325}, +[12631]={290,310,320,330}, +[12632]={295,315,325,335}, +[12633]={300,320,330,340}, +[12636]={300,320,330,340}, +[12639]={300,320,330,340}, +[12640]={300,320,330,340}, +[12641]={300,320,330,340}, +[12643]={250,255,257,260}, +[12644]={250,255,257,260}, +[12645]={275,295,305,315}, +[12655]={250,250,255,260}, +[12764]={260,285,297,310}, +[12769]={270,290,300,310}, +[12772]={270,295,307,320}, +[12773]={275,300,312,325}, +[12774]={275,300,312,325}, +[12775]={280,305,317,330}, +[12776]={280,305,317,330}, +[12777]={280,305,317,330}, +[12779]={285,310,322,335}, +[12781]={285,310,322,335}, +[12782]={290,315,327,340}, +[12783]={300,320,330,340}, +[12784]={300,320,330,340}, +[12790]={300,320,330,340}, +[12792]={290,315,327,340}, +[12794]={300,320,330,340}, +[12795]={300,320,330,340}, +[12796]={300,320,330,340}, +[12797]={300,320,330,340}, +[12798]={300,320,330,340}, +[12802]={300,320,330,340}, +[12803]={275,275,282,290}, +[12808]={275,275,282,290}, +[12810]={250,250,255,260}, +[13423]={250,250,255,260}, +[13442]={255,270,290,310}, +[13443]={260,275,295,315}, +[13444]={295,310,330,350}, +[13445]={265,280,300,320}, +[13446]={275,290,310,330}, +[13447]={265,280,300,320}, +[13452]={280,295,315,335}, +[13453]={275,290,310,330}, +[13454]={285,300,320,340}, +[13455]={280,295,315,335}, +[13456]={290,305,325,345}, +[13457]={290,305,325,345}, +[13458]={290,305,325,345}, +[13459]={290,305,325,345}, +[13460]={290,305,325,345}, +[13461]={290,305,325,345}, +[13462]={285,300,320,340}, +[13503]={300,315,322,330}, +[13506]={300,315,322,330}, +[13510]={300,315,322,330}, +[13511]={300,315,322,330}, +[13512]={300,315,322,330}, +[13513]={300,315,322,330}, +[13851]={175,215,235,255}, +[13856]={255,270,285,300}, +[13857]={260,275,290,305}, +[13858]={260,275,290,305}, +[13860]={265,280,295,310}, +[13863]={275,290,305,320}, +[13864]={280,295,310,325}, +[13865]={285,300,315,330}, +[13866]={295,310,325,340}, +[13867]={300,315,330,345}, +[13868]={255,270,285,300}, +[13869]={255,270,285,300}, +[13870]={265,280,295,310}, +[13871]={280,295,310,325}, +[13927]={225,265,285,305}, +[13928]={240,280,300,320}, +[13929]={240,280,300,320}, +[13930]={225,265,285,305}, +[13931]={250,290,310,330}, +[13932]={250,290,310,330}, +[13933]={275,315,335,355}, +[13934]={275,315,335,355}, +[13935]={275,315,335,355}, +[14042]={260,275,290,305}, +[14043]={270,285,300,315}, +[14044]={275,290,305,320}, +[14045]={280,295,310,325}, +[14046]={260,275,290,305}, +[14048]={250,255,257,260}, +[14100]={270,285,300,315}, +[14101]={270,285,300,315}, +[14103]={275,290,305,320}, +[14104]={290,305,320,335}, +[14106]={300,315,330,345}, +[14107]={275,290,305,320}, +[14108]={285,300,315,330}, +[14111]={290,305,320,335}, +[14112]={300,315,330,345}, +[14128]={300,315,330,345}, +[14130]={300,315,330,345}, +[14132]={275,290,305,320}, +[14134]={275,290,305,320}, +[14136]={285,300,315,330}, +[14137]={290,305,320,335}, +[14138]={300,315,330,345}, +[14139]={300,315,330,345}, +[14140]={300,315,330,345}, +[14141]={275,290,305,320}, +[14142]={270,285,300,315}, +[14143]={265,280,295,310}, +[14144]={290,305,320,335}, +[14146]={300,315,330,345}, +[14152]={300,315,330,345}, +[14153]={300,315,330,345}, +[14154]={300,315,330,345}, +[14155]={300,315,330,345}, +[14156]={300,315,330,345}, +[14342]={250,290,305,320}, +[14529]={260,260,290,320}, +[14530]={290,290,320,350}, +[15045]={260,280,290,300}, +[15046]={270,290,300,310}, +[15047]={300,320,330,340}, +[15048]={285,305,315,325}, +[15049]={295,315,325,335}, +[15050]={290,310,320,330}, +[15051]={300,320,330,340}, +[15052]={300,320,330,340}, +[15053]={285,305,315,325}, +[15054]={270,290,300,310}, +[15055]={300,320,330,340}, +[15056]={285,305,315,325}, +[15057]={275,295,305,315}, +[15058]={295,315,325,335}, +[15059]={300,320,330,340}, +[15060]={285,305,315,325}, +[15061]={270,290,300,310}, +[15062]={300,320,330,340}, +[15063]={290,310,320,330}, +[15064]={275,295,305,315}, +[15065]={285,305,315,325}, +[15066]={290,310,320,330}, +[15067]={270,290,300,310}, +[15068]={300,320,330,340}, +[15069]={285,305,315,325}, +[15070]={295,315,325,335}, +[15071]={275,295,305,315}, +[15072]={280,300,310,320}, +[15073]={275,295,305,315}, +[15074]={265,270,280,290}, +[15075]={290,310,320,330}, +[15076]={265,285,295,305}, +[15077]={255,275,285,295}, +[15078]={275,295,305,315}, +[15079]={285,305,315,325}, +[15080]={295,315,325,335}, +[15081]={300,320,330,340}, +[15082]={280,300,310,320}, +[15083]={260,280,290,300}, +[15084]={265,285,295,305}, +[15085]={300,320,330,340}, +[15086]={280,300,310,320}, +[15087]={290,310,320,330}, +[15088]={300,320,330,340}, +[15090]={300,320,330,340}, +[15091]={270,290,300,310}, +[15092]={275,295,305,315}, +[15093]={280,300,310,320}, +[15094]={290,310,320,330}, +[15095]={300,320,330,340}, +[15096]={300,320,330,340}, +[15138]={300,320,330,340}, +[15141]={300,0,0,0}, +[15407]={250,250,255,260}, +[15564]={250,255,265,275}, +[15802]={290,295,310,325}, +[15846]={250,270,280,290}, +[15869]={100,100,110,120}, +[15870]={150,150,160,170}, +[15871]={200,200,210,220}, +[15872]={275,275,280,285}, +[15992]={250,250,255,260}, +[15993]={260,280,290,300}, +[15994]={260,280,290,300}, +[15995]={260,280,290,300}, +[15996]={265,285,295,305}, +[15997]={285,305,315,325}, +[15999]={270,290,300,310}, +[16000]={275,295,305,315}, +[16004]={275,295,305,315}, +[16005]={285,305,315,325}, +[16006]={285,305,315,325}, +[16007]={300,320,330,340}, +[16008]={290,310,320,330}, +[16009]={290,310,320,330}, +[16022]={300,320,330,340}, +[16023]={275,295,305,315}, +[16040]={300,320,330,340}, +[16206]={275,275,280,285}, +[16207]={290,310,330,350}, +[16766]={225,265,285,305}, +[16979]={300,315,330,345}, +[16980]={300,315,330,345}, +[16982]={295,315,325,335}, +[16983]={300,320,330,340}, +[16984]={300,320,330,340}, +[16988]={300,320,330,340}, +[16989]={295,315,325,335}, +[17013]={300,320,330,340}, +[17014]={295,315,325,335}, +[17015]={300,320,330,340}, +[17016]={300,320,330,340}, +[17193]={300,325,337,350}, +[17197]={1,45,65,85}, +[17198]={35,75,95,115}, +[17222]={200,240,260,280}, +[17704]={190,215,227,240}, +[17708]={190,210,230,250}, +[17716]={190,190,210,230}, +[17721]={190,210,220,230}, +[17723]={190,200,205,210}, +[17771]={300,350,362,375}, +[17967]={300,0,0,0}, +[17968]={300,0,0,0}, +[18045]={225,265,285,305}, +[18168]={300,320,330,340}, +[18232]={300,320,330,340}, +[18238]={200,210,220,230}, +[18251]={300,320,330,340}, +[18253]={300,310,320,330}, +[18254]={275,315,335,355}, +[18258]={275,285,290,295}, +[18262]={300,300,310,320}, +[18263]={300,315,330,345}, +[18282]={300,320,330,340}, +[18283]={300,320,330,340}, +[18294]={215,230,250,270}, +[18405]={300,315,330,345}, +[18407]={300,315,330,345}, +[18408]={300,315,330,345}, +[18409]={300,315,330,345}, +[18413]={300,315,330,345}, +[18486]={300,315,330,345}, +[18504]={300,320,330,340}, +[18506]={300,320,330,340}, +[18508]={300,320,330,340}, +[18509]={300,320,330,340}, +[18510]={300,320,330,340}, +[18511]={300,320,330,340}, +[18587]={265,285,295,305}, +[18588]={200,200,210,220}, +[18594]={275,275,285,295}, +[18631]={260,270,275,280}, +[18634]={260,280,290,300}, +[18637]={275,285,290,295}, +[18638]={290,310,320,330}, +[18639]={300,320,330,340}, +[18641]={250,250,260,270}, +[18645]={265,275,280,285}, +[18660]={260,260,265,270}, +[18662]={150,150,155,160}, +[18948]={155,175,185,195}, +[18984]={260,285,295,305}, +[18986]={260,285,295,305}, +[19026]={250,250,260,270}, +[19043]={290,310,320,330}, +[19044]={290,310,320,330}, +[19047]={290,305,320,335}, +[19048]={300,320,330,340}, +[19049]={300,320,330,340}, +[19050]={300,315,330,345}, +[19051]={290,310,320,330}, +[19052]={290,310,320,330}, +[19056]={290,305,320,335}, +[19057]={300,320,330,340}, +[19058]={300,320,330,340}, +[19059]={300,315,330,345}, +[19148]={300,320,330,340}, +[19149]={300,320,330,340}, +[19156]={300,315,330,345}, +[19157]={300,320,330,340}, +[19162]={300,320,330,340}, +[19163]={300,320,330,340}, +[19164]={300,320,330,340}, +[19165]={300,315,330,345}, +[19166]={300,320,330,340}, +[19167]={300,320,330,340}, +[19168]={300,320,330,340}, +[19169]={300,320,330,340}, +[19170]={300,320,330,340}, +[19440]={300,300,330,360}, +[19682]={300,315,330,345}, +[19683]={300,315,330,345}, +[19684]={300,315,330,345}, +[19685]={300,320,330,340}, +[19686]={300,320,330,340}, +[19687]={300,320,330,340}, +[19688]={300,320,330,340}, +[19689]={300,320,330,340}, +[19690]={300,320,330,340}, +[19691]={300,320,330,340}, +[19692]={300,320,330,340}, +[19693]={300,320,330,340}, +[19694]={300,320,330,340}, +[19695]={300,320,330,340}, +[19931]={300,315,322,330}, +[19998]={300,320,330,340}, +[19999]={300,320,330,340}, +[20002]={275,290,310,330}, +[20004]={290,305,325,345}, +[20007]={275,290,310,330}, +[20008]={285,300,320,340}, +[20039]={300,320,330,340}, +[20074]={150,160,180,200}, +[20295]={300,320,330,340}, +[20296]={280,300,310,320}, +[20380]={300,320,330,340}, +[20452]={285,325,345,365}, +[20476]={300,320,330,340}, +[20477]={300,320,330,340}, +[20478]={300,320,330,340}, +[20479]={300,320,330,340}, +[20480]={300,320,330,340}, +[20481]={300,320,330,340}, +[20537]={300,315,330,345}, +[20538]={300,315,330,345}, +[20539]={300,315,330,345}, +[20549]={300,320,330,340}, +[20550]={300,320,330,340}, +[20551]={300,320,330,340}, +[20575]={100,125,137,150}, +[20744]={45,55,65,75}, +[20745]={150,160,170,180}, +[20746]={200,210,220,230}, +[20747]={250,260,270,280}, +[20748]={300,310,320,330}, +[20749]={300,310,320,330}, +[20750]={275,285,295,305}, +[20844]={1,300,325,350}, +[21023]={300,325,345,365}, +[21072]={80,120,140,160}, +[21154]={250,265,280,295}, +[21217]={175,215,235,255}, +[21277]={250,320,330,340}, +[21278]={300,320,330,340}, +[21340]={260,275,290,305}, +[21341]={280,300,315,330}, +[21342]={300,315,330,345}, +[21542]={250,265,280,295}, +[21546]={250,265,285,305}, +[21557]={125,125,137,150}, +[21558]={125,125,137,150}, +[21559]={125,125,137,150}, +[21569]={225,245,255,265}, +[21570]={275,295,305,315}, +[21571]={225,225,237,250}, +[21574]={225,225,237,250}, +[21576]={225,225,237,250}, +[21589]={175,175,187,200}, +[21590]={175,175,187,200}, +[21592]={175,175,187,200}, +[21714]={275,275,280,285}, +[21716]={275,275,280,285}, +[21718]={275,275,280,285}, +[22191]={300,320,330,340}, +[22194]={300,320,330,340}, +[22195]={300,320,330,340}, +[22196]={300,320,330,340}, +[22197]={300,320,330,340}, +[22198]={300,320,330,340}, +[22246]={225,240,255,270}, +[22248]={275,290,305,320}, +[22249]={300,315,330,345}, +[22251]={275,290,305,320}, +[22252]={300,315,330,345}, +[22383]={300,320,330,340}, +[22384]={300,320,330,340}, +[22385]={300,320,330,340}, +[22652]={300,315,330,345}, +[22654]={300,315,330,345}, +[22655]={300,315,330,345}, +[22658]={300,315,330,345}, +[22660]={300,315,330,345}, +[22661]={300,320,330,340}, +[22662]={300,320,330,340}, +[22663]={300,320,330,340}, +[22664]={300,320,330,340}, +[22665]={300,320,330,340}, +[22666]={300,320,330,340}, +[22669]={300,320,330,340}, +[22670]={300,320,330,340}, +[22671]={300,320,330,340}, +[22756]={300,315,330,345}, +[22757]={300,315,330,345}, +[22758]={300,315,330,345}, +[22759]={300,320,330,340}, +[22760]={300,320,330,340}, +[22761]={300,320,330,340}, +[22762]={300,320,330,340}, +[22763]={300,320,330,340}, +[22764]={300,320,330,340}, +[30818]={35,75,95,115}, +[41308]={90,105,112,120}, +[41309]={95,110,120,130}, +[41310]={105,120,132,145}, +[41311]={105,125,135,145}, +[41312]={120,145,155,165}, +[41313]={110,130,140,150}, +[41314]={125,145,155,165}, +[41315]={125,150,160,170}, +[41316]={125,145,155,165}, +[41318]={125,145,155,165}, +[41319]={125,135,145,155}, +[41320]={125,140,142,145}, +[41321]={235,245,250,255}, +[41322]={175,175,177,180}, +[41323]={225,240,247,255}, +[41324]={245,265,275,285}, +[41325]={135,150,160,170}, +[41326]={125,140,147,155}, +[41327]={225,140,147,155}, +[41328]={175,185,190,195}, +[41329]={135,155,165,175}, +[41330]={280,300,305,310}, +[41331]={150,165,170,175}, +[41332]={150,165,170,175}, +[41340]={190,205,215,225}, +[41341]={200,200,205,210}, +[41342]={190,205,215,225}, +[41343]={200,220,230,240}, +[41344]={150,150,152,155}, +[41345]={215,235,245,255}, +[41346]={210,230,240,250}, +[41347]={220,240,250,260}, +[41349]={220,240,250,260}, +[41673]={175,0,0,0}, +[41674]={175,215,235,255}, +[46600]={100,275,295,310}, +[47408]={1,280,305,330}, +[47409]={1,300,325,350}, +[47410]={300,310,330,350}, +[47412]={300,310,330,350}, +[47414]={300,310,330,350}, +[50237]={300,315,322,330}, +[51256]={75,150,167,185}, +[51262]={75,0,0,0}, +[51264]={75,100,120,140}, +[51268]={75,90,97,105}, +[51284]={75,0,0,0}, +[51312]={120,145,157,170}, +[51313]={120,145,157,170}, +[53015]={300,300,300,300}, +[54009]={1,285,310,335}, +[54010]={1,325,350,375}, +[55043]={300,320,330,340}, +[55046]={300,300,315,330}, +[55048]={300,300,315,330}, +[55050]={300,300,305,310}, +[55052]={300,315,322,330}, +[55054]={300,300,305,310}, +[55056]={300,0,0,0}, +[55058]={300,320,330,340}, +[55060]={300,330,350,370}, +[55141]={185,200,210,220}, +[55142]={155,175,185,195}, +[55143]={170,190,197,205}, +[55144]={155,155,160,165}, +[55145]={175,195,202,210}, +[55146]={175,195,202,210}, +[55147]={175,195,202,210}, +[55148]={165,185,195,205}, +[55150]={1,21,25,30}, +[55151]={70,90,95,100}, +[55152]={150,150,155,160}, +[55153]={200,200,205,210}, +[55154]={250,260,265,270}, +[55156]={1,21,30,40}, +[55157]={1,21,33,45}, +[55158]={15,35,45,55}, +[55159]={25,45,52,60}, +[55160]={35,55,62,70}, +[55161]={50,70,75,80}, +[55162]={50,70,77,85}, +[55163]={50,70,77,85}, +[55164]={50,70,80,90}, +[55165]={55,75,82,90}, +[55166]={60,80,87,95}, +[55167]={60,80,87,95}, +[55168]={65,85,92,100}, +[55169]={65,85,92,100}, +[55170]={70,90,95,100}, +[55171]={80,100,110,120}, +[55172]={80,105,115,125}, +[55173]={85,105,115,125}, +[55174]={90,100,107,115}, +[55175]={100,120,130,140}, +[55176]={100,120,130,140}, +[55178]={270,0,0,0}, +[55180]={255,275,285,295}, +[55195]={175,0,0,0}, +[55196]={215,215,225,235}, +[55197]={220,220,240,260}, +[55198]={275,0,0,0}, +[55199]={265,265,285,305}, +[55200]={280,280,300,320}, +[55202]={160,160,180,200}, +[55204]={230,230,252,275}, +[55210]={185,200,207,215}, +[55211]={175,190,197,205}, +[55212]={200,220,230,240}, +[55213]={205,220,227,235}, +[55228]={260,280,290,300}, +[55241]={290,0,0,0}, +[55242]={210,235,245,255}, +[55243]={235,0,0,0}, +[55244]={275,275,297,320}, +[55248]={275,275,280,300}, +[55255]={210,280,290,300}, +[55256]={255,270,277,285}, +[55258]={265,265,285,305}, +[55259]={300,320,330,340}, +[55260]={260,260,275,290}, +[55261]={300,0,0,0}, +[55263]={300,0,0,0}, +[55264]={300,0,0,0}, +[55265]={300,320,330,340}, +[55266]={250,250,270,290}, +[55267]={280,285,297,310}, +[55268]={265,285,292,300}, +[55269]={260,280,287,295}, +[55271]={265,285,292,300}, +[55272]={300,320,330,340}, +[55273]={280,285,297,310}, +[55316]={75,95,105,115}, +[55317]={85,105,115,125}, +[55318]={100,120,130,140}, +[55319]={115,120,130,140}, +[55320]={130,150,160,170}, +[55321]={145,165,175,185}, +[55322]={160,180,190,200}, +[55323]={170,200,210,220}, +[55324]={180,200,210,220}, +[55325]={190,210,220,230}, +[55326]={100,120,130,140}, +[55327]={140,160,170,180}, +[55328]={170,190,200,210}, +[55329]={80,160,170,180}, +[55330]={105,125,135,145}, +[55331]={140,160,170,180}, +[55332]={170,185,195,205}, +[55333]={85,105,115,125}, +[55334]={120,140,150,160}, +[55335]={160,180,190,200}, +[55336]={200,215,225,235}, +[55337]={105,135,145,155}, +[55338]={145,165,175,185}, +[55339]={200,220,230,240}, +[55340]={85,105,115,125}, +[55341]={135,155,165,175}, +[55359]={300,320,330,340}, +[55360]={290,320,330,340}, +[55361]={300,330,340,350}, +[55362]={285,320,330,340}, +[55363]={300,330,340,350}, +[55364]={300,320,325,330}, +[55365]={300,330,340,350}, +[55366]={300,325,335,345}, +[55367]={300,320,330,340}, +[55368]={300,320,330,340}, +[55518]={300,300,300,300}, +[55519]={300,300,300,300}, +[55520]={300,300,300,300}, +[55521]={300,300,300,300}, +[55522]={300,300,300,300}, +[55523]={300,300,300,300}, +[55524]={300,300,300,300}, +[55525]={300,300,300,300}, +[55526]={300,320,330,340}, +[55527]={300,320,330,340}, +[55528]={300,320,330,340}, +[55529]={300,320,330,340}, +[55530]={300,320,330,340}, +[55531]={300,320,330,340}, +[55532]={300,320,330,340}, +[55533]={300,320,330,340}, +[55534]={300,300,300,300}, +[56000]={150,155,157,160}, +[56001]={175,180,182,185}, +[56002]={200,205,207,210}, +[56003]={150,155,157,160}, +[56004]={200,205,207,210}, +[56005]={200,205,205,205}, +[56006]={200,205,207,210}, +[56007]={250,0,0,0}, +[56008]={235,235,237,240}, +[56009]={250,0,0,0}, +[56010]={270,275,277,280}, +[56011]={275,0,0,0}, +[56012]={260,265,267,270}, +[56013]={285,290,292,295}, +[56014]={300,310,315,320}, +[56015]={275,280,282,285}, +[56016]={295,300,302,305}, +[56017]={275,280,282,285}, +[56018]={250,255,257,260}, +[56019]={235,240,240,240}, +[56020]={200,200,205,210}, +[56023]={190,190,210,230}, +[56031]={300,330,350,370}, +[56032]={300,330,350,370}, +[56033]={295,0,0,0}, +[56034]={195,195,217,240}, +[56035]={290,310,325,340}, +[56036]={260,0,0,0}, +[56037]={80,100,110,120}, +[56038]={100,125,135,145}, +[56039]={110,130,140,150}, +[56040]={135,155,165,175}, +[56041]={160,185,195,205}, +[56042]={160,180,190,200}, +[56043]={170,180,190,200}, +[56044]={80,100,110,120}, +[56045]={115,135,142,150}, +[56046]={135,155,165,175}, +[56047]={185,205,215,225}, +[56048]={190,210,220,230}, +[56049]={200,220,230,240}, +[56050]={200,220,230,240}, +[56051]={200,220,230,240}, +[56052]={200,225,235,245}, +[56053]={210,235,245,255}, +[56054]={180,200,210,220}, +[56055]={200,225,235,245}, +[56056]={225,225,227,230}, +[56057]={290,0,0,0}, +[56058]={175,180,182,185}, +[56059]={300,320,330,340}, +[56060]={300,320,330,340}, +[56061]={300,320,330,340}, +[56062]={300,320,330,340}, +[56063]={300,320,330,340}, +[56064]={300,320,330,340}, +[56065]={290,320,330,340}, +[56066]={290,320,330,340}, +[56067]={290,320,330,340}, +[56068]={250,280,290,300}, +[56069]={230,260,270,280}, +[56070]={200,225,237,250}, +[56071]={240,260,275,290}, +[56072]={300,300,307,315}, +[56073]={245,245,265,285}, +[56074]={135,135,137,140}, +[56075]={270,270,272,275}, +[56076]={300,320,330,340}, +[56077]={300,320,330,340}, +[56089]={210,230,240,250}, +[56090]={265,275,285,295}, +[56091]={125,145,155,165}, +[56092]={300,0,0,0}, +[56093]={290,310,320,330}, +[56094]={285,310,320,330}, +[56095]={230,260,275,290}, +[56096]={300,325,332,340}, +[56112]={185,185,195,205}, +[56113]={200,200,212,225}, +[58112]={170,170,182,195}, +[58134]={185,0,0,0}, +[58304]={290,310,320,330}, +[58305]={290,310,320,330}, +[60007]={300,325,337,350}, +[60008]={300,325,337,350}, +[60009]={300,325,337,350}, +[60010]={300,325,337,350}, +[60098]={250,250,260,270}, +[60099]={250,270,280,290}, +[60287]={300,320,330,340}, +[60288]={300,320,330,340}, +[60289]={300,320,330,340}, +[60290]={300,320,330,340}, +[60291]={300,320,330,340}, +[60292]={300,320,330,340}, +[60293]={300,320,330,340}, +[60294]={225,260,267,275}, +[60573]={290,300,310,320}, +[60574]={290,300,310,320}, +[60575]={290,300,310,320}, +[60576]={275,300,312,325}, +[60577]={285,300,310,320}, +[60578]={275,300,310,320}, +[60907]={300,315,330,345}, +[60908]={300,320,330,340}, +[60909]={300,315,330,345}, +[60910]={300,320,330,340}, +[60976]={300,300,300,300}, +[60977]={300,300,300,300}, +[60978]={300,300,300,300}, +[61181]={300,315,322,330}, +[61182]={275,300,310,320}, +[61183]={300,320,330,340}, +[61185]={300,325,337,350}, +[61186]={300,300,300,300}, +[61187]={300,320,330,340}, +[61188]={300,320,330,340}, +[61216]={300,350,362,375}, +[61224]={300,315,322,330}, +[61225]={300,315,322,330}, +[61229]={300,320,330,340}, +[61230]={300,300,300,300}, +[61356]={300,320,330,340}, +[61357]={300,320,330,340}, +[61358]={300,320,330,340}, +[61359]={300,320,330,340}, +[61360]={300,300,300,300}, +[61361]={300,300,300,300}, +[61362]={300,300,300,300}, +[61363]={300,300,300,300}, +[61364]={300,0,0,0}, +[61365]={300,325,337,350}, +[61366]={300,325,337,350}, +[61367]={300,325,337,350}, +[61648]={290,310,320,330}, +[61649]={300,325,337,350}, +[61732]={300,300,300,300}, +[61779]={25,65,85,105}, +[61780]={90,115,127,140}, +[61781]={140,170,185,200}, +[61782]={185,210,222,235}, +[61783]={240,265,277,290}, +[61784]={300,325,337,350}, +[61785]={300,325,337,350}, +[61810]={300,325,337,350}, +[61818]={300,330,345,360}, +[65000]={295,300,300,300}, +[65001]={300,300,300,300}, +[65002]={295,300,300,300}, +[65003]={300,300,300,300}, +[65004]={300,320,330,340}, +[65005]={250,0,0,0}, +[65006]={300,300,300,300}, +[65007]={270,280,285,290}, +[65008]={300,320,330,340}, +[65009]={205,225,230,235}, +[65010]={30,60,62,65}, +[65011]={75,90,95,100}, +[65012]={120,140,145,150}, +[65013]={180,190,195,200}, +[65014]={300,315,322,330}, +[65015]={290,310,320,330}, +[65019]={245,245,245,245}, +[65021]={300,300,300,300}, +[65022]={265,0,0,0}, +[65023]={255,255,255,255}, +[65024]={300,300,300,300}, +[65025]={300,300,300,300}, +[65026]={300,300,300,300}, +[65027]={300,300,300,300}, +[65032]={1,290,300,310}, +[65035]={300,300,300,300}, +[65036]={300,300,300,300}, +[65037]={300,300,300,300}, +[65038]={300,300,300,300}, +[65039]={300,325,337,350}, +[81030]={20,40,47,55}, +[81031]={65,85,92,100}, +[81032]={35,45,50,55}, +[81061]={270,285,290,295}, +[81062]={280,285,290,295}, +[81063]={270,290,291,295}, +[81064]={280,290,291,295}, +[81065]={275,290,291,295}, +[81066]={285,290,291,295}, +[81092]={45,60,67,75}, +[81093]={25,25,45,65}, +[83280]={230,230,230,230}, +[83281]={235,235,235,235}, +[83282]={230,230,230,230}, +[83283]={225,225,225,225}, +[83284]={225,225,225,225}, +[83285]={230,230,230,230}, +[83286]={205,205,205,205}, +[83287]={210,210,210,210}, +[83288]={200,200,200,200}, +[83289]={200,200,200,200}, +[83290]={205,205,205,205}, +[83291]={205,205,205,205}, +[83292]={265,265,265,265}, +[83293]={265,265,265,265}, +[83294]={270,270,270,270}, +[83295]={260,260,260,260}, +[83296]={260,260,260,260}, +[83297]={265,265,265,265}, +[83309]={300,325,345,365}, +[83400]={210,225,230,235}, +[83401]={210,235,240,245}, +[83402]={205,225,230,235}, +[83403]={200,215,220,225}, +[83404]={200,215,220,225}, +[83405]={200,215,220,225}, +[83410]={220,220,220,220}, +[83411]={220,220,220,220}, +[83412]={225,225,225,225}, +[83413]={225,225,225,225}, +[83414]={230,230,230,230}, +[83415]={230,230,230,230}, +[84040]={300,300,300,300}, +[84041]={200,240,260,280}, +["Accurate Scope"]={180,200,210,220}, +["Admiral's Hat"]={240,255,270,285}, +["Advanced Target Dummy"]={185,185,205,225}, +["Agatestone Crown"]={125,145,155,165}, +["Agitating Poison"]={300,0,0,0}, +["Alarm-O-Bot"]={265,275,280,285}, +["Alchemist's Stone"]={300,315,322,330}, +["Alluring Citrine Choker"]={200,220,230,240}, +["Amber Orb"]={95,110,120,130}, +["Amber Ring"]={60,80,87,95}, +["Ambersap Glazed Boar Ribs"]={175,215,235,255}, +["Amberstone Pendant"]={80,105,115,125}, +["Ancient Dwarven Gemstone"]={185,185,195,205}, +["Annihilator"]={300,320,330,340}, +["Anti-Venom"]={80,80,115,150}, +["Aquadynamic Fish Attractor"]={150,150,160,170}, +["Aquamarine Pendant"]={215,215,225,235}, +["Arcane Bomb"]={300,320,330,340}, +["Arcane Elixir"]={235,250,270,290}, +["Arcane Emerald Gemstone"]={295,300,302,305}, +["Arcanite Belt Buckle"]={300,325,337,350}, +["Arcanite Champion"]={300,320,330,340}, +["Arcanite Dragonling"]={300,320,330,340}, +["Arcanite Reaper"]={300,320,330,340}, +["Arcanite Rod"]={275,275,280,285}, +["Arcanite Skeleton Key"]={275,275,280,285}, +["Arcanum Baton"]={300,320,330,340}, +["Arclight Spanner"]={50,70,80,90}, +["Argent Boots"]={290,305,320,335}, +["Argent Shoulders"]={300,315,330,345}, +["Astral Amulet"]={175,0,0,0}, +["Astronomer Raiments"]={300,315,322,330}, +["Augerer's Boots"]={200,200,200,200}, +["Augerer's Gloves"]={200,200,200,200}, +["Augerer's Hat"]={205,205,205,205}, +["Augerer's Mantle"]={205,205,205,205}, +["Augerer's Robe"]={210,210,210,210}, +["Augerer's Trousers"]={205,205,205,205}, +["Azerothian Ruby Gemstone"]={275,280,282,285}, +["Azure Ring"]={60,80,87,95}, +["Azure Shoulders"]={190,210,225,240}, +["Azure Silk Belt"]={175,195,210,225}, +["Azure Silk Cloak"]={175,195,210,225}, +["Azure Silk Gloves"]={145,165,180,195}, +["Azure Silk Hood"]={145,155,160,165}, +["Azure Silk Pants"]={140,160,175,190}, +["Azure Silk Vest"]={150,170,185,200}, +["Baked Salmon"]={275,315,335,355}, +["Barbaric Belt"]={200,220,230,240}, +["Barbaric Bracers"]={155,175,185,195}, +["Barbaric Gloves"]={150,170,180,190}, +["Barbaric Harness"]={190,210,220,230}, +["Barbaric Iron Boots"]={180,205,217,230}, +["Barbaric Iron Breastplate"]={160,185,197,210}, +["Barbaric Iron Gloves"]={185,210,222,235}, +["Barbaric Iron Helm"]={175,200,212,225}, +["Barbaric Iron Shoulders"]={160,185,197,210}, +["Barbaric Leggings"]={170,190,200,210}, +["Barbaric Linen Vest"]={70,95,112,130}, +["Barbaric Shoulders"]={175,195,205,215}, +["Barbecued Buzzard Wing"]={175,215,235,255}, +["Battery-Powered Crowd Pummeler"]={250,270,280,290}, +["Beautiful Diamond Gemstone"]={270,275,277,280}, +["Beer Basted Boar Ribs"]={25,60,80,100}, +["Belt of the Archmage"]={300,315,330,345}, +["Big Bag of Enchantment"]={300,315,330,345}, +["Big Bear Steak"]={110,150,170,190}, +["Big Black Mace"]={230,255,267,280}, +["Big Bronze Bomb"]={140,140,165,190}, +["Big Bronze Knife"]={105,135,150,165}, +["Big Iron Bomb"]={190,190,210,230}, +["Big Voodoo Cloak"]={240,260,270,280}, +["Big Voodoo Mask"]={220,240,250,260}, +["Big Voodoo Pants"]={240,260,270,280}, +["Big Voodoo Robe"]={215,235,245,255}, +["Binding Signet"]={125,145,155,165}, +["Bindings of Luminance"]={300,320,330,340}, +["Biznicks 247x128 Accurascope"]={300,320,330,340}, +["Black Amnesty"]={300,320,330,340}, +["Black Dragonscale Boots"]={300,320,330,340}, +["Black Dragonscale Breastplate"]={290,310,320,330}, +["Black Dragonscale Leggings"]={300,320,330,340}, +["Black Dragonscale Shoulders"]={300,320,330,340}, +["Black Grasp of the Destroyer"]={300,320,330,340}, +["Black Mageweave Boots"]={230,245,260,275}, +["Black Mageweave Gloves"]={215,230,245,260}, +["Black Mageweave Headband"]={230,245,260,275}, +["Black Mageweave Leggings"]={205,220,235,250}, +["Black Mageweave Robe"]={210,225,240,255}, +["Black Mageweave Shoulders"]={230,245,260,275}, +["Black Mageweave Vest"]={205,220,235,250}, +["Black Silk Pack"]={185,205,220,235}, +["Black Swashbuckler's Shirt"]={200,210,215,220}, +["Black Whelp Cloak"]={100,125,137,150}, +["Black Whelp Tunic"]={100,125,137,150}, +["Blackfury"]={300,320,330,340}, +["Blackguard"]={300,320,330,340}, +["Blackmouth Oil"]={80,80,90,100}, +["Blackrock Ironclamps"]={140,160,170,180}, +["Blackwing Signet of Command"]={300,320,330,340}, +["Blast Shield"]={75,100,120,140}, +["Blazefury Circlet"]={300,320,330,340}, +["Blazing Rapier"]={280,305,317,330}, +["Bleakwood Hew"]={270,290,300,310}, +["Blight"]={250,275,287,300}, +["Blinding Powder"]={1,170,195,220}, +["Blood Sausage"]={60,100,120,140}, +["Blood Talon"]={300,320,330,340}, +["Blood Tiger Breastplate"]={300,320,330,340}, +["Blood Tiger Shoulders"]={300,320,330,340}, +["Bloodfire Circlet"]={200,220,230,240}, +["Bloodletter Razor"]={250,0,0,0}, +["Bloodsoul Breastplate"]={300,320,330,340}, +["Bloodsoul Gauntlets"]={300,320,330,340}, +["Bloodsoul Shoulders"]={300,320,330,340}, +["Bloodstone Warblade"]={225,260,267,275}, +["Bloodvine Boots"]={300,315,330,345}, +["Bloodvine Goggles"]={300,320,330,340}, +["Bloodvine Leggings"]={300,315,330,345}, +["Bloodvine Lens"]={300,320,330,340}, +["Bloodvine Vest"]={300,315,330,345}, +["Bloody Belt Buckle"]={300,325,337,350}, +["Blue Dragonscale Boots"]={290,310,320,330}, +["Blue Dragonscale Breastplate"]={285,305,315,325}, +["Blue Dragonscale Leggings"]={300,320,330,340}, +["Blue Dragonscale Shoulders"]={295,315,325,335}, +["Blue Firework"]={150,150,162,175}, +["Blue Glittering Axe"]={220,245,257,270}, +["Blue Linen Robe"]={70,95,112,130}, +["Blue Linen Shirt"]={40,65,82,100}, +["Blue Linen Vest"]={55,80,97,115}, +["Blue Overalls"]={100,125,142,160}, +["Blue Rocket Cluster"]={225,225,237,250}, +["Blue Starfire"]={265,265,285,305}, +["Boiled Clams"]={50,90,110,130}, +["Bolt of Linen Cloth"]={1,25,37,50}, +["Bolt of Mageweave"]={175,180,182,185}, +["Bolt of Runecloth"]={250,255,257,260}, +["Bolt of Silk Cloth"]={125,135,140,145}, +["Bolt of Woolen Cloth"]={75,90,97,105}, +["Boots of Darkness"]={140,160,175,190}, +["Boots of the Enchanter"]={175,195,210,225}, +["Boots of the Wind"]={255,255,255,255}, +["Bottomless Bag"]={300,315,330,345}, +["Bramblewood Belt"]={300,320,330,340}, +["Bramblewood Boots"]={300,320,330,340}, +["Bramblewood Helm"]={300,320,330,340}, +["Breastplate of the Earth"]={265,0,0,0}, +["Bright Copper Necklace"]={65,85,92,100}, +["Bright Copper Ring"]={15,35,45,55}, +["Bright Yellow Shirt"]={135,145,150,155}, +["Bright-Eye Goggles"]={175,195,205,215}, +["Brightcloth Cloak"]={275,290,305,320}, +["Brightcloth Gloves"]={270,285,300,315}, +["Brightcloth Pants"]={290,305,320,335}, +["Brightcloth Robe"]={270,285,300,315}, +["Brilliant Mana Oil"]={300,310,320,330}, +["Brilliant Opal Gemstone"]={235,235,237,240}, +["Brilliant Smallfish"]={1,45,65,85}, +["Brilliant Wizard Oil"]={300,310,320,330}, +["Bristle Whisker Catfish"]={100,140,160,180}, +["Bronze Axe"]={115,145,160,175}, +["Bronze Battle Axe"]={135,165,180,195}, +["Bronze Belt Buckle"]={90,115,127,140}, +["Bronze Bruiser"]={120,140,145,150}, +["Bronze Cuffed Bangles"]={105,120,132,145}, +["Bronze Framework"]={145,145,170,195}, +["Bronze Greatsword"]={130,160,175,190}, +["Bronze Mace"]={110,140,155,170}, +["Bronze Scepter"]={110,130,140,150}, +["Bronze Shortsword"]={120,150,165,180}, +["Bronze Tube"]={105,105,130,155}, +["Bronze Warhammer"]={125,155,170,185}, +["Brown Linen Pants"]={30,55,72,90}, +["Brown Linen Robe"]={30,55,72,90}, +["Brown Linen Shirt"]={1,35,47,60}, +["Brown Linen Vest"]={10,45,57,70}, +["Bulky Copper Ring"]={25,25,45,65}, +["Burning Star Gemstone"]={225,225,227,230}, +["Carrion Surprise"]={175,215,235,255}, +["Catseye Elixir"]={200,220,240,260}, +["Catseye Ultra Goggles"]={220,240,250,260}, +["Cenarion Herb Bag"]={275,290,305,320}, +["Centaur Battle Harness"]={300,320,330,340}, +["Centaur Hoof Circlet"]={160,180,190,200}, +["Charged Scale of Onyxia"]={300,0,0,0}, +["Charred Wolf Meat"]={1,45,65,85}, +["Chimeric Boots"]={275,295,305,315}, +["Chimeric Gloves"]={265,270,280,290}, +["Chimeric Leggings"]={280,300,310,320}, +["Chimeric Vest"]={290,310,320,330}, +["Chromatic Cloak"]={300,320,330,340}, +["Chromatic Gauntlets"]={300,320,330,340}, +["Chromatic Leggings"]={300,300,300,300}, +["Cindercloth Boots"]={245,260,275,290}, +["Cindercloth Cloak"]={275,290,305,320}, +["Cindercloth Gloves"]={270,285,300,315}, +["Cindercloth Pants"]={280,295,310,325}, +["Cindercloth Robe"]={225,240,255,270}, +["Cindercloth Vest"]={260,275,290,305}, +["Cinderfall Band"]={260,280,290,300}, +["Circlet of Dampening"]={135,155,165,175}, +["Clam Chowder"]={90,130,150,170}, +["Cloak of Fire"]={275,290,305,320}, +["Cloak of Warding"]={300,315,330,345}, +["Coarse Blasting Powder"]={75,85,90,95}, +["Coarse Dynamite"]={75,90,97,105}, +["Coarse Gemstone Cluster"]={125,140,142,145}, +["Coarse Grinding Stone"]={75,75,87,100}, +["Coarse Gritted Paper"]={70,90,95,100}, +["Coarse Sharpening Stone"]={65,65,72,80}, +["Coarse Weightstone"]={65,65,72,80}, +["Colorful Kilt"]={120,145,162,180}, +["Comfortable Leather Hat"]={200,220,230,240}, +["Compact Harvest Reaper Kit"]={175,175,195,215}, +["Concoction of the Arcane Giant"]={300,310,330,350}, +["Concoction of the Dreamwater"]={300,310,330,350}, +["Concoction of the Emerald Mongoose"]={300,310,330,350}, +["Cooked Crab Claw"]={85,125,145,165}, +["Cooked Glossy Mightfish"]={225,265,285,305}, +["Copper Axe"]={20,60,80,100}, +["Copper Bangle"]={1,21,33,45}, +["Copper Battle Axe"]={35,75,95,115}, +["Copper Belt Buckle"]={25,65,85,105}, +["Copper Bracers"]={1,20,40,60}, +["Copper Chain Belt"]={35,75,95,115}, +["Copper Chain Boots"]={20,60,80,100}, +["Copper Chain Pants"]={1,50,70,90}, +["Copper Chain Vest"]={35,75,95,115}, +["Copper Claymore"]={30,70,90,110}, +["Copper Dagger"]={30,70,90,110}, +["Copper Knuckles"]={30,60,62,65}, +["Copper Mace"]={15,55,75,95}, +["Copper Modulator"]={65,95,110,125}, +["Copper Shortsword"]={25,65,85,105}, +["Copper Staff"]={45,60,67,75}, +["Copper Tube"]={50,80,95,110}, +["Core Armor Kit"]={300,320,330,340}, +["Core Felcloth Bag"]={300,315,330,345}, +["Core Marksman Rifle"]={300,320,330,340}, +["Corehound Belt"]={300,320,330,340}, +["Corehound Boots"]={295,315,325,335}, +["Corehound Gloves"]={300,300,300,300}, +["Corrosive Poison"]={1,280,305,330}, +["Corrosive Poison II"]={1,300,325,350}, +["Corruption"]={290,315,327,340}, +["Cosmic Headdress"]={300,300,300,300}, +["Cosmic Leggings"]={300,300,300,300}, +["Cosmic Mantle"]={300,300,300,300}, +["Cosmic Vest"]={300,300,300,300}, +["Coyote Steak"]={50,90,110,130}, +["Crab Cake"]={75,115,135,155}, +["Crafted Heavy Shot"]={75,85,90,95}, +["Crafted Light Shot"]={1,30,45,60}, +["Crafted Solid Shot"]={125,125,135,145}, +["Craftsman's Monocle"]={185,205,215,225}, +["Crawford Apple Tarte"]={175,0,0,0}, +["Crimson Silk Belt"]={175,195,210,225}, +["Crimson Silk Cloak"]={180,200,215,230}, +["Crimson Silk Gloves"]={210,225,240,255}, +["Crimson Silk Pantaloons"]={195,215,225,235}, +["Crimson Silk Robe"]={205,220,235,250}, +["Crimson Silk Shoulders"]={190,210,225,240}, +["Crimson Silk Vest"]={185,205,215,225}, +["Crippling Poison"]={1,125,150,175}, +["Crippling Poison II"]={230,275,300,325}, +["Crispy Bat Wing"]={1,45,65,85}, +["Crispy Lizard Tail"]={100,140,160,180}, +["Crocolisk Gumbo"]={120,160,180,200}, +["Crocolisk Steak"]={80,120,140,160}, +["Crown of Elegance"]={230,260,270,280}, +["Crown of Molten Ascension"]={300,320,330,340}, +["Crown of the Illustrious Queen"]={300,320,330,340}, +["Crude Scope"]={60,90,105,120}, +["Crystal Earring"]={185,205,215,225}, +["Crystalfire Armlets"]={255,275,285,295}, +["Crystalweft Bracers"]={280,285,297,310}, +["Cured Heavy Hide"]={150,160,165,170}, +["Cured Light Hide"]={35,55,65,75}, +["Cured Medium Hide"]={100,115,122,130}, +["Cured Rugged Hide"]={250,250,255,260}, +["Cured Thick Hide"]={200,200,200,200}, +["Curiously Tasty Omelet"]={130,170,190,210}, +["Dalaran Wizard Disguise"]={1,0,0,0}, +["Danonzo's Tel'Abim Delight"]={300,300,300,300}, +["Danonzo's Tel'Abim Medley"]={300,300,300,300}, +["Danonzo's Tel'Abim Surprise"]={300,300,300,300}, +["Dark Iron Belt Buckle"]={275,275,280,285}, +["Dark Iron Bomb"]={285,305,315,325}, +["Dark Iron Boots"]={300,320,330,340}, +["Dark Iron Bracers"]={295,315,325,335}, +["Dark Iron Destroyer"]={300,320,330,340}, +["Dark Iron Dwarf Disguise"]={1,0,0,0}, +["Dark Iron Gauntlets"]={300,320,330,340}, +["Dark Iron Helm"]={300,320,330,340}, +["Dark Iron Leggings"]={300,320,330,340}, +["Dark Iron Mail"]={270,290,300,310}, +["Dark Iron Plate"]={285,305,315,325}, +["Dark Iron Pulverizer"]={265,285,295,305}, +["Dark Iron Reaver"]={300,320,330,340}, +["Dark Iron Rifle"]={275,295,305,315}, +["Dark Iron Shoulders"]={280,300,310,320}, +["Dark Iron Signet Ring"]={290,320,330,340}, +["Dark Iron Sunderer"]={275,295,305,315}, +["Dark Leather Belt"]={125,150,162,175}, +["Dark Leather Boots"]={100,125,137,150}, +["Dark Leather Cloak"]={110,135,147,160}, +["Dark Leather Gloves"]={120,155,167,180}, +["Dark Leather Pants"]={115,140,152,165}, +["Dark Leather Shoulders"]={140,165,177,190}, +["Dark Leather Tunic"]={100,125,137,150}, +["Dark Silk Shirt"]={155,165,170,175}, +["Darkrune Breastplate"]={300,320,330,340}, +["Darkrune Gauntlets"]={300,320,330,340}, +["Darkrune Helm"]={300,320,330,340}, +["Darksoul Breastplate"]={300,320,330,340}, +["Darksoul Leggings"]={300,320,330,340}, +["Darksoul Shoulders"]={300,320,330,340}, +["Darkspear"]={300,320,330,340}, +["Dawn Treaders"]={290,310,320,330}, +["Dawn's Edge"]={275,300,312,325}, +["Dawnbright Cuffs"]={115,135,142,150}, +["Dawnbringer Shoulders"]={290,310,320,330}, +["Dawnstone Hammer"]={300,325,337,350}, +["Dazzling Aquamarine Loop"]={190,210,220,230}, +["Dazzling Mithril Rapier"]={240,265,277,290}, +["Dazzling Moonstone Band"]={130,150,160,170}, +["Deadly Blunderbuss"]={105,130,142,155}, +["Deadly Bronze Poniard"]={125,155,170,185}, +["Deadly Poison"]={130,175,200,225}, +["Deadly Poison II"]={170,215,240,265}, +["Deadly Poison III"]={210,255,280,305}, +["Deadly Poison IV"]={250,295,320,345}, +["Deadly Poison V"]={1,300,325,350}, +["Deadly Scope"]={210,230,240,250}, +["Deep Sapphire Circlet"]={290,320,330,340}, +["Deepdive Helmet"]={230,250,260,270}, +["Deepmist Choker"]={85,105,115,125}, +["Defias Disguise"]={1,0,0,0}, +["Delicate Arcanite Converter"]={285,305,315,325}, +["Delicate Mithril Amulet"]={180,200,210,220}, +["Demon Forged Breastplate"]={285,305,315,325}, +["Dense Blasting Powder"]={250,250,255,260}, +["Dense Dynamite"]={250,250,260,270}, +["Dense Gemstone Cluster"]={235,240,240,240}, +["Dense Grinding Stone"]={250,255,257,260}, +["Dense Gritted Paper"]={250,260,265,270}, +["Dense Sharpening Stone"]={250,255,257,260}, +["Dense Weightstone"]={250,255,257,260}, +["Depthstalker Helmet"]={300,0,0,0}, +["Deviate Scale Belt"]={115,140,152,165}, +["Deviate Scale Cloak"]={90,120,135,150}, +["Deviate Scale Gloves"]={105,130,142,155}, +["Devilsaur Gauntlets"]={290,310,320,330}, +["Devilsaur Leggings"]={300,320,330,340}, +["Dig Rat Stew"]={90,130,150,170}, +["Dimensional Ripper - Everlook"]={260,0,0,0}, +["Dirge's Kickin' Chimaerok Chops"]={300,325,345,365}, +["Discolored Healing Potion"]={50,80,100,120}, +["Discombobulator Ray"]={160,180,190,200}, +["Dissolvent Poison"]={1,285,310,335}, +["Dissolvent Poison II"]={1,325,350,375}, +["Diviner's Boots"]={225,225,225,225}, +["Diviner's Cowl"]={230,230,230,230}, +["Diviner's Epaulets"]={230,230,230,230}, +["Diviner's Mitts"]={225,225,225,225}, +["Diviner's Pantaloons"]={230,230,230,230}, +["Diviner's Robes"]={235,235,235,235}, +["Double-stitched Woolen Shoulders"]={110,135,152,170}, +["Draenethyst Baton"]={200,225,235,245}, +["Dragonbreath Chili"]={200,240,260,280}, +["Dragonmaw Armor Kit"]={175,175,182,190}, +["Dragonmaw Gloves"]={170,170,182,195}, +["Dragonscale Belt Buckle"]={235,235,240,245}, +["Dragonscale Breastplate"]={255,275,285,295}, +["Dragonscale Gauntlets"]={225,245,255,265}, +["Dragonscale Leggings"]={245,245,245,245}, +["Dream's Herald"]={300,320,330,340}, +["Dreamhide"]={300,320,330,340}, +["Dreamhide Belt"]={300,320,330,340}, +["Dreamhide Bracers"]={300,320,330,340}, +["Dreamhide Leggings"]={300,320,330,340}, +["Dreamhide Mantle"]={300,320,330,340}, +["Dreamless Sleep Potion"]={230,245,265,285}, +["Dreamscale Breastplate"]={300,320,330,340}, +["Dreamshard Elixir"]={300,315,322,330}, +["Dreamsteel Belt Buckle"]={300,325,337,350}, +["Dreamsteel Boots"]={300,325,337,350}, +["Dreamsteel Bracers"]={300,325,337,350}, +["Dreamsteel Leggings"]={300,325,337,350}, +["Dreamsteel Mantle"]={300,0,0,0}, +["Dreamthread"]={300,300,300,300}, +["Dreamthread Bracers"]={300,300,300,300}, +["Dreamthread Gloves"]={300,300,300,300}, +["Dreamthread Kilt"]={300,300,300,300}, +["Dreamthread Mantle"]={300,300,300,300}, +["Dreamweave Circlet"]={250,265,280,295}, +["Dreamweave Gloves"]={225,240,255,270}, +["Dreamweave Vest"]={225,240,255,270}, +["Dreary Opal Gemstone"]={270,270,272,275}, +["Dry Pork Ribs"]={80,120,140,160}, +["Dusky Belt"]={195,215,225,235}, +["Dusky Boots"]={200,220,230,240}, +["Dusky Bracers"]={185,205,215,225}, +["Dusky Leather Armor"]={175,195,205,215}, +["Dusky Leather Leggings"]={165,185,195,205}, +["Dustguider Sash"]={300,315,330,345}, +["EZ-Thro Dynamite"]={100,115,122,130}, +["EZ-Thro Dynamite II"]={200,200,210,220}, +["Earthen Leather Shoulders"]={135,160,172,185}, +["Earthen Silk Belt"]={195,215,230,245}, +["Earthen Vest"]={170,190,205,220}, +["Earthguard Tunic"]={300,300,300,300}, +["Earthrock Loop"]={100,120,130,140}, +["Ebon Hand"]={300,320,330,340}, +["Ebon Ring"]={75,95,105,115}, +["Ebon Shiv"]={255,280,292,305}, +["Edge of Winter"]={190,215,227,240}, +["Egg Nog"]={35,75,95,115}, +["Elaborate Golden Bracelets"]={200,220,230,240}, +["Elegant Emerald Gemstone"]={250,0,0,0}, +["Elemental Sharpening Stone"]={300,300,310,320}, +["Elixir of Agility"]={185,205,225,245}, +["Elixir of Brute Force"]={275,290,310,330}, +["Elixir of Defense"]={130,155,175,195}, +["Elixir of Demonslaying"]={250,265,285,305}, +["Elixir of Detect Demon"]={250,265,285,305}, +["Elixir of Detect Lesser Invisibility"]={195,215,235,255}, +["Elixir of Detect Undead"]={230,245,265,285}, +["Elixir of Dream Vision"]={240,255,275,295}, +["Elixir of Firepower"]={140,165,185,205}, +["Elixir of Fortitude"]={175,195,215,235}, +["Elixir of Frost Power"]={190,210,230,250}, +["Elixir of Giant Growth"]={90,120,140,160}, +["Elixir of Giants"]={245,260,280,300}, +["Elixir of Greater Agility"]={240,255,275,295}, +["Elixir of Greater Arcane Power"]={300,300,315,330}, +["Elixir of Greater Defense"]={195,215,235,255}, +["Elixir of Greater Firepower"]={250,265,285,305}, +["Elixir of Greater Frost Power"]={300,300,315,330}, +["Elixir of Greater Intellect"]={235,250,270,290}, +["Elixir of Greater Nature Power"]={300,315,322,330}, +["Elixir of Greater Water Breathing"]={215,230,250,270}, +["Elixir of Lesser Agility"]={140,165,185,205}, +["Elixir of Lion's Strength"]={1,55,75,95}, +["Elixir of Minor Agility"]={50,80,100,120}, +["Elixir of Minor Defense"]={1,55,75,95}, +["Elixir of Minor Fortitude"]={50,80,100,120}, +["Elixir of Ogre's Strength"]={150,175,195,215}, +["Elixir of Poison Resistance"]={120,145,165,185}, +["Elixir of Rapid Growth"]={200,200,212,225}, +["Elixir of Shadow Power"]={250,265,285,305}, +["Elixir of Superior Defense"]={265,280,300,320}, +["Elixir of Water Breathing"]={90,120,140,160}, +["Elixir of Wisdom"]={90,120,140,160}, +["Elixir of the Mongoose"]={280,295,315,335}, +["Elixir of the Sages"]={265,280,300,320}, +["Embergem Cuffs"]={300,320,330,340}, +["Emberstone Idol"]={220,240,250,260}, +["Emberstone Studded Ring"]={225,240,247,255}, +["Embossed Leather Boots"]={55,85,100,115}, +["Embossed Leather Cloak"]={60,90,105,120}, +["Embossed Leather Gloves"]={55,85,100,115}, +["Embossed Leather Pants"]={75,105,120,135}, +["Embossed Leather Vest"]={40,70,85,100}, +["Emerald Monarch's Glow"]={300,320,330,340}, +["Empowered Domination Rod"]={300,330,340,350}, +["Empowering Herbal Salad"]={300,325,345,365}, +["Enchant 2H Weapon - Agility"]={290,310,330,350}, +["Enchant 2H Weapon - Greater Impact"]={240,260,280,300}, +["Enchant 2H Weapon - Impact"]={200,220,240,260}, +["Enchant 2H Weapon - Lesser Impact"]={145,170,190,210}, +["Enchant 2H Weapon - Lesser Intellect"]={100,130,150,170}, +["Enchant 2H Weapon - Lesser Spirit"]={110,135,155,175}, +["Enchant 2H Weapon - Major Intellect"]={300,320,340,360}, +["Enchant 2H Weapon - Major Spirit"]={300,320,340,360}, +["Enchant 2H Weapon - Minor Impact"]={100,130,150,170}, +["Enchant 2H Weapon - Minor Intellect"]={75,130,150,170}, +["Enchant 2H Weapon - Superior Impact"]={295,315,335,355}, +["Enchant Boots - Agility"]={235,255,275,295}, +["Enchant Boots - Greater Agility"]={295,315,335,355}, +["Enchant Boots - Greater Spirit"]={300,300,300,300}, +["Enchant Boots - Greater Stamina"]={260,280,300,320}, +["Enchant Boots - Lesser Agility"]={160,180,200,220}, +["Enchant Boots - Lesser Intellect"]={170,170,182,195}, +["Enchant Boots - Lesser Spirit"]={190,210,230,250}, +["Enchant Boots - Lesser Stamina"]={170,190,210,230}, +["Enchant Boots - Major Intellect"]={300,300,300,300}, +["Enchant Boots - Minor Agility"]={125,150,170,190}, +["Enchant Boots - Minor Speed"]={225,245,265,285}, +["Enchant Boots - Minor Stamina"]={125,150,170,190}, +["Enchant Boots - Spirit"]={275,295,315,335}, +["Enchant Boots - Stamina"]={215,235,255,275}, +["Enchant Boots - Superior Stamina"]={300,300,300,300}, +["Enchant Boots - Vampirism"]={300,300,300,300}, +["Enchant Bracer - Agility"]={185,205,225,245}, +["Enchant Bracer - Deflection"]={235,255,275,295}, +["Enchant Bracer - Greater Agility"]={300,300,300,300}, +["Enchant Bracer - Greater Deflection"]={300,300,300,300}, +["Enchant Bracer - Greater Intellect"]={255,275,295,315}, +["Enchant Bracer - Greater Spirit"]={220,240,260,280}, +["Enchant Bracer - Greater Stamina"]={245,265,285,305}, +["Enchant Bracer - Greater Strength"]={240,260,280,300}, +["Enchant Bracer - Healing Power"]={300,320,340,360}, +["Enchant Bracer - Intellect"]={210,230,250,270}, +["Enchant Bracer - Lesser Deflection"]={170,190,210,230}, +["Enchant Bracer - Lesser Intellect"]={150,175,195,215}, +["Enchant Bracer - Lesser Spirit"]={120,145,165,185}, +["Enchant Bracer - Lesser Stamina"]={130,155,175,195}, +["Enchant Bracer - Lesser Strength"]={140,165,185,205}, +["Enchant Bracer - Mana Regeneration"]={290,310,330,350}, +["Enchant Bracer - Minor Agility"]={80,115,135,155}, +["Enchant Bracer - Minor Deflect"]={1,80,100,120}, +["Enchant Bracer - Minor Health"]={1,70,90,110}, +["Enchant Bracer - Minor Spirit"]={60,105,125,145}, +["Enchant Bracer - Minor Stamina"]={50,100,120,140}, +["Enchant Bracer - Minor Strength"]={80,115,135,155}, +["Enchant Bracer - Spell Power"]={300,300,300,300}, +["Enchant Bracer - Spirit"]={165,185,205,225}, +["Enchant Bracer - Stamina"]={170,190,210,230}, +["Enchant Bracer - Strength"]={180,200,220,240}, +["Enchant Bracer - Superior Spirit"]={270,290,310,330}, +["Enchant Bracer - Superior Stamina"]={300,320,340,360}, +["Enchant Bracer - Superior Strength"]={295,315,335,355}, +["Enchant Bracer - Vampirism"]={185,205,225,245}, +["Enchant Chest - Greater Health"]={160,180,200,220}, +["Enchant Chest - Greater Mana"]={185,205,225,245}, +["Enchant Chest - Greater Stats"]={300,320,340,360}, +["Enchant Chest - Health"]={120,145,165,185}, +["Enchant Chest - Lesser Absorption"]={140,165,185,205}, +["Enchant Chest - Lesser Health"]={60,105,125,145}, +["Enchant Chest - Lesser Mana"]={80,115,135,155}, +["Enchant Chest - Lesser Stats"]={200,220,240,260}, +["Enchant Chest - Major Health"]={275,295,315,335}, +["Enchant Chest - Major Mana"]={290,310,330,350}, +["Enchant Chest - Mana"]={145,170,190,210}, +["Enchant Chest - Mighty Mana"]={300,300,300,300}, +["Enchant Chest - Minor Absorption"]={40,90,110,130}, +["Enchant Chest - Minor Health"]={15,70,90,110}, +["Enchant Chest - Minor Mana"]={20,80,100,120}, +["Enchant Chest - Minor Stats"]={150,175,195,215}, +["Enchant Chest - Stats"]={245,265,285,305}, +["Enchant Chest - Superior Health"]={220,240,260,280}, +["Enchant Chest - Superior Mana"]={230,250,270,290}, +["Enchant Cloak - Defense"]={155,175,195,215}, +["Enchant Cloak - Dodge"]={300,320,340,360}, +["Enchant Cloak - Fire Resistance"]={175,195,215,235}, +["Enchant Cloak - Greater Arcane Resistance"]={300,300,300,300}, +["Enchant Cloak - Greater Defense"]={205,225,245,265}, +["Enchant Cloak - Greater Fire Resistance"]={300,320,340,360}, +["Enchant Cloak - Greater Nature Resistance"]={300,320,340,360}, +["Enchant Cloak - Greater Resistance"]={265,285,305,325}, +["Enchant Cloak - Lesser Agility"]={225,245,265,285}, +["Enchant Cloak - Lesser Fire Resistance"]={125,150,170,190}, +["Enchant Cloak - Lesser Protection"]={115,140,160,180}, +["Enchant Cloak - Lesser Shadow Resistance"]={135,160,180,200}, +["Enchant Cloak - Minor Agility"]={110,135,155,175}, +["Enchant Cloak - Minor Protection"]={70,110,130,150}, +["Enchant Cloak - Minor Resistance"]={45,95,115,135}, +["Enchant Cloak - Resistance"]={205,225,245,265}, +["Enchant Cloak - Stealth"]={300,320,340,360}, +["Enchant Cloak - Subtlety"]={300,320,340,360}, +["Enchant Cloak - Superior Defense"]={285,305,325,345}, +["Enchant Gloves - Advanced Herbalism"]={225,245,265,285}, +["Enchant Gloves - Advanced Mining"]={215,235,255,275}, +["Enchant Gloves - Agility"]={210,230,250,270}, +["Enchant Gloves - Arcane Power"]={300,300,300,300}, +["Enchant Gloves - Fire Power"]={300,320,340,360}, +["Enchant Gloves - Fishing"]={145,170,190,210}, +["Enchant Gloves - Frost Power"]={300,320,340,360}, +["Enchant Gloves - Greater Agility"]={270,290,310,330}, +["Enchant Gloves - Greater Strength"]={295,315,335,355}, +["Enchant Gloves - Healing Power"]={300,320,340,360}, +["Enchant Gloves - Herbalism"]={145,170,190,210}, +["Enchant Gloves - Major Strength"]={300,300,300,300}, +["Enchant Gloves - Mining"]={145,170,190,210}, +["Enchant Gloves - Minor Haste"]={250,270,290,310}, +["Enchant Gloves - Nature Power"]={300,300,300,300}, +["Enchant Gloves - Riding Skill"]={250,270,290,310}, +["Enchant Gloves - Shadow Power"]={300,320,340,360}, +["Enchant Gloves - Skinning"]={200,220,240,260}, +["Enchant Gloves - Strength"]={225,245,265,285}, +["Enchant Gloves - Superior Agility"]={300,320,340,360}, +["Enchant Gloves - Threat"]={300,320,340,360}, +["Enchant Shield - Frost Resistance"]={235,255,275,295}, +["Enchant Shield - Greater Spirit"]={230,250,270,290}, +["Enchant Shield - Greater Stamina"]={265,285,305,325}, +["Enchant Shield - Lesser Block"]={195,215,235,255}, +["Enchant Shield - Lesser Protection"]={115,140,160,180}, +["Enchant Shield - Lesser Spirit"]={130,155,175,195}, +["Enchant Shield - Lesser Stamina"]={155,175,195,215}, +["Enchant Shield - Minor Stamina"]={105,130,150,170}, +["Enchant Shield - Spirit"]={180,200,220,240}, +["Enchant Shield - Stamina"]={210,230,250,270}, +["Enchant Shield - Superior Spirit"]={280,300,320,340}, +["Enchant Weapon - Agility"]={290,310,330,350}, +["Enchant Weapon - Crusader"]={300,320,340,360}, +["Enchant Weapon - Demonslaying"]={230,250,270,290}, +["Enchant Weapon - Fiery Weapon"]={265,285,305,325}, +["Enchant Weapon - Greater Striking"]={245,265,285,305}, +["Enchant Weapon - Healing Power"]={300,320,340,360}, +["Enchant Weapon - Icy Chill"]={285,305,325,345}, +["Enchant Weapon - Lesser Beastslayer"]={175,195,215,235}, +["Enchant Weapon - Lesser Elemental Slayer"]={175,195,215,235}, +["Enchant Weapon - Lesser Striking"]={140,165,185,205}, +["Enchant Weapon - Lifestealing"]={300,320,340,360}, +["Enchant Weapon - Mighty Intellect"]={300,320,340,360}, +["Enchant Weapon - Mighty Spirit"]={300,320,340,360}, +["Enchant Weapon - Minor Beastslayer"]={90,120,140,160}, +["Enchant Weapon - Minor Striking"]={90,120,140,160}, +["Enchant Weapon - Spell Power"]={300,320,340,360}, +["Enchant Weapon - Strength"]={290,310,330,350}, +["Enchant Weapon - Striking"]={195,215,235,255}, +["Enchant Weapon - Superior Striking"]={300,320,340,360}, +["Enchant Weapon - Unholy Weapon"]={295,315,335,355}, +["Enchant Weapon - Winter's Might"]={190,210,230,250}, +["Enchanted Armor Kit"]={300,320,330,340}, +["Enchanted Battlehammer"]={280,305,317,330}, +["Enchanted Bracelets"]={125,145,155,165}, +["Enchanted Emerald Gemstone"]={250,255,257,260}, +["Enchanted Gemstone Oil"]={275,275,280,300}, +["Enchanted Leather"]={250,250,255,260}, +["Enchanted Mageweave Pouch"]={225,240,255,270}, +["Enchanted Runecloth Bag"]={275,290,305,320}, +["Enchanted Thorium"]={250,250,255,260}, +["Enchanted Thorium Belt Buckle"]={285,285,297,310}, +["Enchanted Thorium Breastplate"]={300,320,330,340}, +["Enchanted Thorium Helm"]={300,320,330,340}, +["Enchanted Thorium Leggings"]={300,320,330,340}, +["Enchanter's Cowl"]={165,185,200,215}, +["Encrusted Bronze Staff"]={100,120,130,140}, +["Encrusted Copper Bangle"]={50,70,75,80}, +["Encrusted Gemstone Ring"]={300,330,350,370}, +["Essence Infused Leather Gloves"]={300,300,305,310}, +["Eternal Dreamstone Shard"]={300,300,300,300}, +["Ethereal Frostspark Crown"]={280,285,297,310}, +["Ethereal Helmet"]={300,300,300,300}, +["Ethereal Leggings"]={300,300,300,300}, +["Ethereal Shoulder Pads"]={300,300,300,300}, +["Ethereal Tunic"]={300,300,300,300}, +["Explosive Sheep"]={150,175,187,200}, +["Facetted Moonstone Brooch"]={185,200,207,215}, +["Fangclaw Relic"]={120,140,150,160}, +["Farraki Ceremony Totem"]={140,160,170,180}, +["Feathered Breastplate"]={250,270,280,290}, +["Felcloth Bag"]={280,300,315,330}, +["Felcloth Boots"]={285,300,315,330}, +["Felcloth Gloves"]={300,315,330,345}, +["Felcloth Hood"]={290,305,320,335}, +["Felcloth Pants"]={275,290,305,320}, +["Felcloth Robe"]={300,315,330,345}, +["Felcloth Shoulders"]={300,315,330,345}, +["Festive Red Dress"]={250,0,0,0}, +["Festive Red Pant Suit"]={250,265,280,295}, +["Field Repair Bot 74A"]={300,320,330,340}, +["Fiery Chain Breastplate"]={300,325,337,350}, +["Fiery Chain Girdle"]={295,315,325,335}, +["Fiery Chain Shoulders"]={300,320,330,340}, +["Fiery Plate Gauntlets"]={290,310,320,330}, +["Filet of Redgill"]={225,265,285,305}, +["Fillet of Frenzy"]={50,90,110,130}, +["Fine Leather Belt"]={80,110,125,140}, +["Fine Leather Boots"]={90,120,135,150}, +["Fine Leather Cloak"]={85,105,120,135}, +["Fine Leather Gloves"]={75,105,120,135}, +["Fine Leather Pants"]={105,130,142,155}, +["Fine Leather Tunic"]={85,115,130,145}, +["Fire Goggles"]={205,225,235,245}, +["Fire Oil"]={130,150,160,170}, +["Fire Protection Potion"]={165,210,230,250}, +["Firework Cluster Launcher"]={275,295,305,315}, +["Firework Launcher"]={225,245,255,265}, +["Flame Deflector"]={125,125,150,175}, +["Flamewrath Leggings"]={300,300,300,300}, +["Flarecore Boots"]={300,300,300,300}, +["Flarecore Gloves"]={300,315,330,345}, +["Flarecore Leggings"]={300,315,330,345}, +["Flarecore Mantle"]={300,315,330,345}, +["Flarecore Robe"]={300,315,330,345}, +["Flarecore Wraps"]={300,315,330,345}, +["Flash Bomb"]={185,185,205,225}, +["Flask of Chromatic Resistance"]={300,315,322,330}, +["Flask of Distilled Wisdom"]={300,315,322,330}, +["Flask of Petrification"]={300,315,322,330}, +["Flask of Supreme Power"]={300,315,322,330}, +["Flask of the Titans"]={300,315,322,330}, +["Flawless Arcanite Rifle"]={300,320,330,340}, +["Flawless Black Gemstone"]={285,290,292,295}, +["Fletcher's Gloves"]={125,150,162,175}, +["Flying Tiger Goggles"]={100,130,145,160}, +["Force Reactive Disk"]={300,320,330,340}, +["Formal White Shirt"]={170,180,185,190}, +["Free Action Potion"]={150,175,195,215}, +["Frost Leather Cloak"]={180,200,210,220}, +["Frost Oil"]={200,220,240,260}, +["Frost Protection Potion"]={190,205,225,245}, +["Frost Tiger Blade"]={200,225,237,250}, +["Frostbound Slasher"]={180,190,195,200}, +["Frostguard"]={300,320,330,340}, +["Frostsaber Boots"]={275,295,305,315}, +["Frostsaber Gloves"]={295,315,325,335}, +["Frostsaber Leggings"]={285,305,315,325}, +["Frostsaber Tunic"]={300,320,330,340}, +["Frostweave Gloves"]={265,280,295,310}, +["Frostweave Pants"]={280,295,310,325}, +["Frostweave Robe"]={255,270,285,300}, +["Frostweave Tunic"]={255,270,285,300}, +["Fury of the Timbermaw"]={290,310,320,330}, +["Gaea's Embrace"]={300,315,330,345}, +["Garnet Guardian Staff"]={290,0,0,0}, +["Gauntlets of the Sea"]={230,250,260,270}, +["Gem Encrusted Choker"]={160,180,190,200}, +["Gem-studded Leather Belt"]={185,205,215,225}, +["Gemkeeper's Folio"]={235,0,0,0}, +["Gemmed Citrine Pendant"]={160,160,180,200}, +["Gemmed Copper Gauntlets"]={60,100,120,140}, +["Gemstone Compendium"]={275,275,297,320}, +["Ghost Dye"]={245,260,280,300}, +["Ghostweave Belt"]={265,280,295,310}, +["Ghostweave Gloves"]={270,285,300,315}, +["Ghostweave Pants"]={290,305,320,335}, +["Ghostweave Vest"]={275,290,305,320}, +["Giant Clam Scorcho"]={175,215,235,255}, +["Gift of Arthas"]={240,255,275,295}, +["Giga-Charged Arcane Reflector"]={290,310,320,330}, +["Gilneas Hot Stew"]={200,240,260,280}, +["Gingerbread Cookie"]={1,45,65,85}, +["Girdle of Insight"]={300,320,330,340}, +["Girdle of the Dawn"]={290,310,320,330}, +["Glacial Cloak"]={300,315,330,345}, +["Glacial Gloves"]={300,315,330,345}, +["Glacial Vest"]={300,315,330,345}, +["Glacial Wrists"]={300,315,330,345}, +["Gleaming Chain"]={80,100,110,120}, +["Gleaming Jade Gemstone"]={175,180,182,185}, +["Gleaming Silver Necklace"]={135,155,165,175}, +["Glinting Steel Dagger"]={180,205,217,230}, +["Glittering Sapphire Gemstone"]={290,0,0,0}, +["Gloomweed Bindings"]={80,160,170,180}, +["Gloomy Diamond Gemstone"]={260,265,267,270}, +["Gloves of Manathirst"]={75,150,167,185}, +["Gloves of Meditation"]={130,150,165,180}, +["Gloves of Spell Mastery"]={300,315,330,345}, +["Gloves of Unwinding Mystery"]={300,300,300,300}, +["Gloves of the Dawn"]={300,320,330,340}, +["Gloves of the Greatfather"]={190,210,220,230}, +["Glowing Ruby Gemstone"]={200,205,207,210}, +["Glyph Codex"]={260,280,287,295}, +["Gnomish Battle Chicken"]={230,250,260,270}, +["Gnomish Cloaking Device"]={200,220,230,240}, +["Gnomish Death Ray"]={240,260,270,280}, +["Gnomish Goggles"]={210,230,240,250}, +["Gnomish Harm Prevention Belt"]={215,235,245,255}, +["Gnomish Mind Control Cap"]={235,255,265,275}, +["Gnomish Net-o-Matic Projector"]={210,230,240,250}, +["Gnomish Rocket Boots"]={225,245,255,265}, +["Gnomish Shrink Ray"]={205,225,235,245}, +["Gnomish Universal Remote"]={125,150,162,175}, +["Goblin Bomb Dispenser"]={230,230,250,270}, +["Goblin Construction Helmet"]={205,225,235,245}, +["Goblin Deviled Clams"]={125,165,185,205}, +["Goblin Dragon Gun"]={240,260,270,280}, +["Goblin Jumper Cables"]={165,165,180,200}, +["Goblin Jumper Cables XL"]={265,285,295,305}, +["Goblin Land Mine"]={195,215,225,235}, +["Goblin Mining Helmet"]={205,225,235,245}, +["Goblin Mortar"]={205,225,235,245}, +["Goblin Radio"]={225,240,250,260}, +["Goblin Rocket Boots"]={225,245,255,265}, +["Goblin Rocket Fuel"]={210,225,245,265}, +["Goblin Rocket Fuel Recipe"]={205,205,205,205}, +["Goblin Rocket Helmet"]={245,265,275,285}, +["Goblin Sapper Charge"]={205,205,225,245}, +["Gold Belt Buckle"]={175,175,180,185}, +["Gold Power Core"]={150,150,170,190}, +["Goldcrest Amulet"]={170,180,190,200}, +["Golden Iron Destroyer"]={170,195,207,220}, +["Golden Jade Ring"]={210,235,245,255}, +["Golden Mantle of the Dawn"]={300,320,330,340}, +["Golden Rod"]={150,155,157,160}, +["Golden Runed Ring"]={285,310,320,330}, +["Golden Scale Boots"]={200,225,237,250}, +["Golden Scale Bracers"]={185,210,222,235}, +["Golden Scale Coif"]={190,215,227,240}, +["Golden Scale Cuirass"]={195,220,232,245}, +["Golden Scale Gauntlets"]={205,225,235,245}, +["Golden Scale Leggings"]={170,195,207,220}, +["Golden Scale Shoulders"]={175,200,212,225}, +["Golden Scepter of Authority"]={260,0,0,0}, +["Golden Skeleton Key"]={150,150,160,170}, +["Goldenshade Quartz Crown"]={175,195,202,210}, +["Goldfire Crystal Bracelet"]={155,155,160,165}, +["Goldthorn Tea"]={175,175,190,205}, +["Gooey Spider Cake"]={110,150,170,190}, +["Gordok Ogre Suit"]={275,285,290,295}, +["Goretusk Liver Pie"]={50,90,110,130}, +["Gorgeous Mountain Gemstone"]={300,330,345,360}, +["Graceful Agate Gemstone"]={135,135,137,140}, +["Grail of Forgotten Memories"]={300,330,340,350}, +["Grandstaff of the Shen'dralar Elder"]={300,330,350,370}, +["Gray Woolen Robe"]={105,130,147,165}, +["Gray Woolen Shirt"]={100,110,120,130}, +["Great Rage Potion"]={175,195,215,235}, +["Greater Adept's Robe"]={115,140,157,175}, +["Greater Arcane Elixir"]={285,300,320,340}, +["Greater Arcane Protection Potion"]={290,305,325,345}, +["Greater Binding Signet"]={210,230,240,250}, +["Greater Dreamless Sleep Potion"]={275,290,310,330}, +["Greater Fire Protection Potion"]={290,305,325,345}, +["Greater Frost Protection Potion"]={290,305,325,345}, +["Greater Healing Potion"]={155,175,195,215}, +["Greater Holy Protection Potion"]={290,305,325,345}, +["Greater Magic Wand"]={70,110,130,150}, +["Greater Mana Potion"]={205,220,240,260}, +["Greater Mystic Wand"]={175,195,215,235}, +["Greater Nature Protection Potion"]={290,305,325,345}, +["Greater Shadow Protection Potion"]={290,305,325,345}, +["Greater Stoneshield Potion"]={280,295,315,335}, +["Green Dragonscale Breastplate"]={260,280,290,300}, +["Green Dragonscale Gauntlets"]={280,300,310,320}, +["Green Dragonscale Leggings"]={270,290,300,310}, +["Green Firework"]={150,150,162,175}, +["Green Holiday Shirt"]={190,200,205,210}, +["Green Iron Boots"]={145,175,190,205}, +["Green Iron Bracers"]={165,190,202,215}, +["Green Iron Gauntlets"]={150,180,195,210}, +["Green Iron Hauberk"]={180,205,217,230}, +["Green Iron Helm"]={170,195,207,220}, +["Green Iron Leggings"]={155,180,192,205}, +["Green Iron Shoulders"]={160,185,197,210}, +["Green Leather Armor"]={155,175,185,195}, +["Green Leather Belt"]={160,180,190,200}, +["Green Leather Bracers"]={180,200,210,220}, +["Green Lens"]={245,265,275,285}, +["Green Linen Bracers"]={60,85,102,120}, +["Green Linen Shirt"]={70,95,112,130}, +["Green Rocket Cluster"]={225,225,237,250}, +["Green Silk Armor"]={165,185,200,215}, +["Green Silk Pack"]={175,195,210,225}, +["Green Silken Shoulders"]={180,200,215,230}, +["Green Tinted Goggles"]={150,175,187,200}, +["Green Whelp Armor"]={175,195,205,215}, +["Green Whelp Bracers"]={190,210,220,230}, +["Green Woolen Bag"]={95,120,137,155}, +["Green Woolen Robe"]={90,0,0,0}, +["Green Woolen Vest"]={85,110,127,145}, +["Grifter's Belt"]={200,215,220,225}, +["Grifter's Boots"]={200,215,220,225}, +["Grifter's Cover"]={210,225,230,235}, +["Grifter's Gauntlets"]={200,215,220,225}, +["Grifter's Leggings"]={205,225,230,235}, +["Grifter's Tunic"]={210,235,240,245}, +["Grilled Squid"]={240,280,300,320}, +["Guardbreaker Charm"]={300,320,325,330}, +["Guardian Armor"]={175,195,205,215}, +["Guardian Belt"]={170,190,200,210}, +["Guardian Cloak"]={185,205,215,225}, +["Guardian Gloves"]={190,210,220,230}, +["Guardian Leather Bracers"]={195,215,225,235}, +["Guardian Pants"]={160,180,190,200}, +["Gurubashi Gumbo"]={300,300,300,300}, +["Gurubashi Mojo Madness"]={300,315,322,330}, +["Gyrochronatom"]={170,170,190,210}, +["Gyrofreeze Ice Reflector"]={260,280,290,300}, +["Gyromatic Micro-Adjustor"]={175,175,195,215}, +["Hammer of the Titans"]={300,320,330,340}, +["Handful of Copper Bolts"]={30,45,52,60}, +["Hands of Darkness"]={145,165,180,195}, +["Handstitched Leather Belt"]={25,55,70,85}, +["Handstitched Leather Boots"]={1,40,55,70}, +["Handstitched Leather Bracers"]={1,40,55,70}, +["Handstitched Leather Cloak"]={1,40,55,70}, +["Handstitched Leather Pants"]={15,45,60,75}, +["Handstitched Leather Vest"]={1,40,55,70}, +["Handstitched Linen Britches"]={70,95,112,130}, +["Hardened Iron Shortsword"]={160,185,197,210}, +["Harness of the High Thane"]={300,320,330,340}, +["Harpy Talon Ring"]={145,165,175,185}, +["Hateforge Belt"]={275,300,312,325}, +["Hateforge Boots"]={275,300,310,320}, +["Hateforge Cuirass"]={290,300,310,320}, +["Hateforge Grips"]={285,300,310,320}, +["Hateforge Helmet"]={290,300,310,320}, +["Hateforge Leggings"]={290,300,310,320}, +["Healing Potion"]={110,135,155,175}, +["Heart of the Sea"]={200,220,230,240}, +["Heartseeker"]={300,320,330,340}, +["Heavy Armor Kit"]={150,170,180,190}, +["Heavy Blasting Powder"]={125,125,135,145}, +["Heavy Bronze Mace"]={130,160,175,190}, +["Heavy Copper Broadsword"]={95,135,155,175}, +["Heavy Copper Maul"]={65,105,125,145}, +["Heavy Crocolisk Stew"]={150,160,180,200}, +["Heavy Dynamite"]={125,125,135,145}, +["Heavy Earthen Gloves"]={145,170,182,195}, +["Heavy Gemstone Cluster"]={150,150,152,155}, +["Heavy Grinding Stone"]={125,125,137,150}, +["Heavy Gritted Paper"]={150,150,155,160}, +["Heavy Kodo Stew"]={200,240,260,280}, +["Heavy Leather"]={150,150,155,160}, +["Heavy Leather Ammo Pouch"]={150,170,180,190}, +["Heavy Leather Ball"]={150,150,155,160}, +["Heavy Linen Bandage"]={40,50,75,100}, +["Heavy Linen Gloves"]={35,60,77,95}, +["Heavy Mageweave Bandage"]={240,240,270,300}, +["Heavy Mithril Axe"]={210,235,247,260}, +["Heavy Mithril Boots"]={235,255,265,275}, +["Heavy Mithril Breastplate"]={230,250,260,270}, +["Heavy Mithril Gauntlet"]={205,225,235,245}, +["Heavy Mithril Helm"]={245,255,265,275}, +["Heavy Mithril Pants"]={210,230,240,250}, +["Heavy Mithril Shoulder"]={205,225,235,245}, +["Heavy Obsidian Belt"]={300,320,330,340}, +["Heavy Quiver"]={150,170,180,190}, +["Heavy Runecloth Bandage"]={290,290,320,350}, +["Heavy Scorpid Belt"]={280,300,310,320}, +["Heavy Scorpid Bracers"]={255,275,285,295}, +["Heavy Scorpid Gauntlets"]={275,295,305,315}, +["Heavy Scorpid Helm"]={295,315,325,335}, +["Heavy Scorpid Leggings"]={285,305,315,325}, +["Heavy Scorpid Shoulders"]={300,320,330,340}, +["Heavy Scorpid Vest"]={265,285,295,305}, +["Heavy Sharpening Stone"]={125,125,132,140}, +["Heavy Silk Bandage"]={180,180,210,240}, +["Heavy Timbermaw Belt"]={290,310,320,330}, +["Heavy Timbermaw Boots"]={300,320,330,340}, +["Heavy Weightstone"]={125,125,132,140}, +["Heavy Wool Bandage"]={115,115,150,185}, +["Heavy Woolen Cloak"]={100,125,142,160}, +["Heavy Woolen Gloves"]={85,110,127,145}, +["Heavy Woolen Pants"]={110,135,152,170}, +["Helm of Fire"]={250,270,280,290}, +["Helm of the Great Chief"]={300,320,330,340}, +["Herb Baked Egg"]={1,45,65,85}, +["Herbalist's Gloves"]={135,160,172,185}, +["Hi-Explosive Bomb"]={235,235,255,275}, +["Hi-Impact Mithril Slugs"]={210,210,230,250}, +["Hide of the Wild"]={300,320,330,340}, +["Hillman's Belt"]={120,145,157,170}, +["Hillman's Cloak"]={150,170,180,190}, +["Hillman's Leather Gloves"]={145,170,182,195}, +["Hillman's Leather Vest"]={100,125,137,150}, +["Hillman's Shoulders"]={130,155,167,180}, +["Holy Protection Potion"]={100,130,150,170}, +["Hot Lion Chops"]={125,175,195,215}, +["Hot Smoked Bass"]={240,280,300,320}, +["Hot Wolf Ribs"]={175,215,235,255}, +["Huge Thorium Battleaxe"]={280,305,317,330}, +["Hydrathorn Bracers"]={105,125,135,145}, +["Hyper-Radiant Flame Reflector"]={290,310,320,330}, +["Hypertech Battery Pack"]={250,250,260,270}, +["Ice Deflector"]={155,175,185,195}, +["Icebane Bracers"]={300,320,330,340}, +["Icebane Breastplate"]={300,320,330,340}, +["Icebane Gauntlets"]={300,320,330,340}, +["Icy Cloak"]={200,220,235,250}, +["Icy Scale Bracers"]={300,320,330,340}, +["Icy Scale Breastplate"]={300,320,330,340}, +["Icy Scale Gauntlets"]={300,320,330,340}, +["Illuminated Gemstone"]={200,205,205,205}, +["Imperial Plate Belt"]={265,285,295,305}, +["Imperial Plate Boots"]={295,315,325,335}, +["Imperial Plate Bracers"]={270,290,300,310}, +["Imperial Plate Chest"]={300,320,330,340}, +["Imperial Plate Gauntlets"]={270,280,285,290}, +["Imperial Plate Helm"]={295,315,325,335}, +["Imperial Plate Leggings"]={300,320,330,340}, +["Imperial Plate Shoulders"]={265,285,295,305}, +["Inferno Gloves"]={300,315,330,345}, +["Inlaid Copper Ring"]={35,55,62,70}, +["Inlaid Mithril Cylinder"]={200,225,237,250}, +["Inlaid Mithril Cylinder Plans"]={205,205,205,205}, +["Inlaid Thorium Hammer"]={270,295,307,320}, +["Inscribed Runic Bracers"]={300,320,330,340}, +["Instant Poison"]={1,125,150,175}, +["Instant Poison II"]={120,165,190,215}, +["Instant Poison III"]={160,205,230,255}, +["Instant Poison IV"]={200,245,270,295}, +["Instant Poison V"]={240,285,340,335}, +["Instant Poison VI"]={280,325,350,375}, +["Intricate Gyroscope Goggles"]={300,320,330,340}, +["Invisibility Potion"]={235,250,270,290}, +["Invulnerable Mail"]={300,320,330,340}, +["Iridescent Hammer"]={140,170,185,200}, +["Iron Belt Buckle"]={140,170,185,200}, +["Iron Buckle"]={150,150,152,155}, +["Iron Counterweight"]={165,190,202,215}, +["Iron Grenade"]={175,175,195,215}, +["Iron Shield Spike"]={150,180,195,210}, +["Iron Strut"]={160,160,170,180}, +["Ironbloom Ring"]={190,205,215,225}, +["Ironfeather Breastplate"]={290,310,320,330}, +["Ironfeather Shoulders"]={270,290,300,310}, +["Ironforge Breastplate"]={100,140,160,180}, +["Ironforge Chain"]={70,110,130,150}, +["Ironforge Gauntlets"]={140,0,0,0}, +["Ironsun Citrine Ring"]={185,200,210,220}, +["Ironvine Belt"]={300,320,330,340}, +["Ironvine Breastplate"]={300,320,330,340}, +["Ironvine Gloves"]={300,320,330,340}, +["Jade Harmony Circlet"]={170,190,197,205}, +["Jade Serpentblade"]={175,200,212,225}, +["Jagged Obsidian Shield"]={300,320,330,340}, +["Jewelry Lens"]={125,140,147,155}, +["Jewelry Scope"]={225,140,147,155}, +["Jungle Stew"]={175,215,235,255}, +["Kaldorei Spider Kabob"]={10,50,70,90}, +["Kodo Hide Bag"]={40,70,85,100}, +["Large Blue Rocket"]={175,175,187,200}, +["Large Blue Rocket Cluster"]={275,275,280,285}, +["Large Copper Bomb"]={105,105,130,155}, +["Large Green Rocket"]={175,175,187,200}, +["Large Green Rocket Cluster"]={275,275,280,285}, +["Large Red Rocket"]={175,175,187,200}, +["Large Red Rocket Cluster"]={275,275,280,285}, +["Large Seaforium Charge"]={200,200,220,240}, +["Lava Belt"]={300,320,330,340}, +["Lavender Mageweave Shirt"]={230,235,240,245}, +["Lavish Gemmed Necklace"]={80,100,110,120}, +["Le Fishe Au Chocolat"]={300,300,300,300}, +["Lean Venison"]={110,150,170,190}, +["Lean Wolf Steak"]={125,165,185,205}, +["Lesser Fortification Ring"]={50,70,77,85}, +["Lesser Healing Potion"]={55,85,105,125}, +["Lesser Invisibility Potion"]={165,185,205,225}, +["Lesser Magic Wand"]={10,75,95,115}, +["Lesser Mana Oil"]={250,260,270,280}, +["Lesser Mana Potion"]={120,145,165,185}, +["Lesser Mystic Wand"]={155,175,195,215}, +["Lesser Stoneshield Potion"]={215,230,250,270}, +["Lesser Wizard Oil"]={200,210,220,230}, +["Lesser Wizard's Robe"]={135,155,170,185}, +["Ley-Kissed Drape"]={300,300,300,300}, +["Lifelike Mechanical Toad"]={265,285,295,305}, +["Light Armor Kit"]={1,30,45,60}, +["Light Leather"]={1,20,30,40}, +["Light Leather Bracers"]={70,100,115,130}, +["Light Leather Pants"]={95,125,140,155}, +["Light Leather Quiver"]={30,60,75,90}, +["Light Obsidian Belt"]={300,320,330,340}, +["Lil' Smoky"]={205,205,205,205}, +["Limited Invulnerability Potion"]={250,275,295,315}, +["Linen Bag"]={45,70,87,105}, +["Linen Bandage"]={1,30,45,60}, +["Linen Belt"]={15,50,67,85}, +["Linen Boots"]={65,90,107,125}, +["Linen Cloak"]={1,35,47,60}, +["Lionheart Helm"]={300,320,330,340}, +["Living Action Potion"]={285,300,320,340}, +["Living Breastplate"]={300,320,330,340}, +["Living Leggings"]={285,305,315,325}, +["Living Shoulders"]={270,290,300,310}, +["Lobster Stew"]={275,315,335,355}, +["Loch Frenzy Delight"]={50,90,110,130}, +["Long Silken Cloak"]={185,205,220,235}, +["Longjaw Mud Snapper"]={50,90,110,130}, +["Lordaeron Breastplate"]={100,275,295,310}, +["Lovingly Crafted Boomstick"]={120,145,157,170}, +["Lucidity Potion"]={300,315,322,330}, +["Lynxstep Boots"]={75,0,0,0}, +["Mageblood Potion"]={275,290,310,330}, +["Mageweave Bag"]={225,240,255,270}, +["Mageweave Bandage"]={210,210,240,270}, +["Magic Resistance Potion"]={210,225,245,265}, +["Major Healing Potion"]={275,290,310,330}, +["Major Mana Potion"]={295,310,330,350}, +["Major Recombobulator"]={275,285,290,295}, +["Major Rejuvenation Potion"]={300,310,320,330}, +["Major Troll's Blood Potion"]={290,305,325,345}, +["Malachite Ring"]={20,40,47,55}, +["Mana Binding Signet"]={230,260,275,290}, +["Mana Potion"]={160,180,200,220}, +["Mantle of Centaur Authority"]={300,320,330,340}, +["Mantle of the Timbermaw"]={300,315,330,345}, +["Marine Root"]={200,215,225,235}, +["Marine's Demise"]={190,210,220,230}, +["Maritime Gumbo"]={35,75,95,115}, +["Massive Iron Axe"]={185,210,222,235}, +["Massive Jewel Circlet"]={300,0,0,0}, +["Master Engineer's Goggles"]={290,310,320,330}, +["Mastercrafted Diamond Bangles"]={300,325,332,340}, +["Mastercrafted Diamond Crown"]={300,320,330,340}, +["Masterwork Stormhammer"]={300,320,330,340}, +["Masterwork Target Dummy"]={275,295,305,315}, +["Mechanical Dragonling"]={200,220,230,240}, +["Mechanical Repair Kit"]={200,200,220,240}, +["Mechanical Squirrel"]={75,105,120,135}, +["Medallion of Flame"]={110,130,140,150}, +["Medium Armor Kit"]={100,115,122,130}, +["Medium Leather"]={100,100,105,110}, +["Might of the Timbermaw"]={290,310,320,330}, +["Mightfish Steak"]={275,315,335,355}, +["Mighty Iron Hammer"]={145,175,190,205}, +["Mighty Rage Potion"]={255,270,290,310}, +["Mighty Troll's Blood Potion"]={180,200,220,240}, +["Mind-numbing Poison"]={1,150,175,200}, +["Mind-numbing Poison II"]={1,215,240,265}, +["Mind-numbing Poison III"]={1,285,310,335}, +["Minor Healing Potion"]={1,55,75,95}, +["Minor Magic Resistance Potion"]={110,135,155,175}, +["Minor Mana Oil"]={150,160,170,180}, +["Minor Mana Potion"]={25,65,85,105}, +["Minor Recombobulator"]={140,165,177,190}, +["Minor Rejuvenation Potion"]={40,70,90,110}, +["Minor Trollblood Ring"]={50,70,80,90}, +["Minor Wizard Oil"]={45,55,65,75}, +["Mistwood Tiara"]={105,135,145,155}, +["Mithril Belt Buckle"]={185,210,222,235}, +["Mithril Blackstone Necklace"]={245,265,275,285}, +["Mithril Blunderbuss"]={205,225,235,245}, +["Mithril Casing"]={215,215,235,255}, +["Mithril Coif"]={230,250,260,270}, +["Mithril Frag Bomb"]={215,215,235,255}, +["Mithril Gyro-Shot"]={245,245,265,285}, +["Mithril Headed Trout"]={175,215,235,255}, +["Mithril Heavy-bore Rifle"]={220,240,250,260}, +["Mithril Mechanical Dragonling"]={250,270,280,290}, +["Mithril Scale Bracers"]={215,235,245,255}, +["Mithril Scale Gloves"]={220,240,250,260}, +["Mithril Scale Pants"]={210,230,240,250}, +["Mithril Scale Shoulders"]={235,255,265,275}, +["Mithril Shield Spike"]={215,235,245,255}, +["Mithril Spurs"]={235,255,265,275}, +["Mithril Tube"]={195,195,215,235}, +["Molten Belt"]={300,320,330,340}, +["Molten Helm"]={300,320,330,340}, +["Molten Leggings"]={300,300,300,300}, +["Monastery Emberbrace"]={170,185,195,205}, +["Mongoose Boots"]={300,320,330,340}, +["Monster Omelet"]={225,265,285,305}, +["Mooncloth"]={250,290,305,320}, +["Mooncloth Bag"]={300,315,330,345}, +["Mooncloth Boots"]={290,295,310,325}, +["Mooncloth Circlet"]={300,315,330,345}, +["Mooncloth Gloves"]={300,315,330,345}, +["Mooncloth Leggings"]={290,305,320,335}, +["Mooncloth Robe"]={300,315,330,345}, +["Mooncloth Shoulders"]={300,315,330,345}, +["Mooncloth Vest"]={300,315,330,345}, +["Moonglow Vest"]={90,115,130,145}, +["Moonlight Staff"]={125,150,160,170}, +["Moonlit Charm"]={275,0,0,0}, +["Moonsight Rifle"]={145,170,182,195}, +["Moonsteel Broadsword"]={180,205,217,230}, +["Murloc Fin Soup"]={90,130,150,170}, +["Murloc Scale Belt"]={90,120,135,150}, +["Murloc Scale Bracers"]={190,210,220,230}, +["Murloc Scale Breastplate"]={95,125,140,155}, +["Mystery Stew"]={175,215,235,255}, +["Nature Protection Potion"]={190,210,230,250}, +["Netherbane Rod"]={160,180,190,200}, +["Nightfall"]={300,320,330,340}, +["Nightfin Soup"]={250,290,310,330}, +["Nightscape Boots"]={235,255,265,275}, +["Nightscape Cloak"]={230,250,260,270}, +["Nightscape Headband"]={205,225,235,245}, +["Nightscape Pants"]={230,250,260,270}, +["Nightscape Shoulders"]={210,230,240,250}, +["Nightscape Tunic"]={205,225,235,245}, +["Nimble Leather Gloves"]={120,145,157,170}, +["Obsidian Belt Buckle"]={300,300,310,320}, +["Obsidian Brooch"]={175,190,197,205}, +["Obsidian Mail Tunic"]={300,320,330,340}, +["Ocean's Gaze"]={190,190,210,230}, +["Ocean's Wrath"]={115,120,130,140}, +["Ogre Bone Band"]={170,200,210,220}, +["Oil of Immolation"]={205,220,240,260}, +["Onyxia Scale Breastplate"]={300,0,0,0}, +["Onyxia Scale Cloak"]={300,320,330,340}, +["Opal Guided Bangles"]={250,280,290,300}, +["Opaline Illuminator"]={210,235,245,255}, +["Opalstone Circle"]={290,320,330,340}, +["Orange Mageweave Shirt"]={215,220,225,230}, +["Orange Martial Shirt"]={220,225,230,235}, +["Orb of Clairvoyance"]={285,320,330,340}, +["Orcish War Leggings"]={210,250,260,270}, +["Ornament of Restraint"]={245,245,265,285}, +["Ornate Bloodstone Dagger"]={300,320,330,340}, +["Ornate Mithril Boots"]={210,265,275,285}, +["Ornate Mithril Bracelets"]={200,225,237,250}, +["Ornate Mithril Breastplate"]={210,260,270,280}, +["Ornate Mithril Crown"]={210,230,240,250}, +["Ornate Mithril Gloves"]={220,240,250,260}, +["Ornate Mithril Helm"]={210,265,275,285}, +["Ornate Mithril Pants"]={220,240,250,260}, +["Ornate Mithril Scepter"]={200,220,230,240}, +["Ornate Mithril Shoulders"]={225,245,255,265}, +["Ornate Spyglass"]={135,160,172,185}, +["Ornate Thorium Handaxe"]={275,300,312,325}, +["Otherworldly Breastplate"]={300,320,330,340}, +["Otherworldly Coif"]={300,320,330,340}, +["Otherworldly Leggings"]={300,320,330,340}, +["Otherworldly Spaulders"]={300,320,330,340}, +["Parachute Cloak"]={225,245,255,265}, +["Patterned Bronze Bracers"]={120,150,165,180}, +["Pauldron of Deflection"]={300,315,322,330}, +["Pauldrons of the Timbermaw"]={300,325,337,350}, +["Pearl-clasped Cloak"]={90,115,132,150}, +["Pearl-handled Dagger"]={110,140,155,170}, +["Peasant Disguise"]={1,0,0,0}, +["Pendant of Arcane Radiance"]={280,300,305,310}, +["Pendant of Instability"]={300,300,307,315}, +["Pendant of Midnight"]={120,145,155,165}, +["Peon Disguise"]={1,0,0,0}, +["Persuader"]={300,320,330,340}, +["Pet Bombling"]={205,205,205,205}, +["Phantom Blade"]={245,270,282,295}, +["Philosophers' Stone"]={225,0,0,0}, +["Phoenix Gloves"]={125,150,167,185}, +["Phoenix Pants"]={125,150,167,185}, +["Pilferer's Gloves"]={140,165,177,190}, +["Pillager's Amice"]={265,265,265,265}, +["Pillager's Grips"]={260,260,260,260}, +["Pillager's Hood"]={265,265,265,265}, +["Pillager's Pantaloons"]={265,265,265,265}, +["Pillager's Robe"]={270,270,270,270}, +["Pillager's Shoes"]={260,260,260,260}, +["Pink Mageweave Shirt"]={235,240,245,250}, +["Poached Sunscale Salmon"]={250,290,310,330}, +["Polar Bracers"]={300,320,330,340}, +["Polar Gloves"]={300,320,330,340}, +["Polar Tunic"]={300,320,330,340}, +["Polished Steel Boots"]={185,210,222,235}, +["Portable Bronze Mortar"]={165,185,195,205}, +["Portable Wormhole Generator - Orgrimmar"]={125,0,0,0}, +["Portable Wormhole Generator - Stormwind"]={125,0,0,0}, +["Potion of Quickness"]={300,315,322,330}, +["Powerful Anti-Venom"]={300,300,330,360}, +["Powerful Citrine Pendant"]={175,195,202,210}, +["Powerful Seaforium Charge"]={275,275,285,295}, +["Powerful Smelling Salts"]={300,300,330,360}, +["Practice Lock"]={100,115,122,130}, +["Precision Jewelry Kit"]={175,185,190,195}, +["Primal Batskin Bracers"]={300,320,330,340}, +["Primal Batskin Gloves"]={300,320,330,340}, +["Primal Batskin Jerkin"]={300,320,330,340}, +["Primalist's Boots"]={275,290,291,295}, +["Primalist's Gloves"]={270,285,290,295}, +["Primalist's Headdress"]={270,290,291,295}, +["Primalist's Pants"]={280,290,291,295}, +["Primalist's Shoulders"]={280,285,290,295}, +["Primalist's Vest"]={285,290,291,295}, +["Prism Amulet"]={265,265,285,305}, +["Prismatic Scale Barbute"]={300,300,305,310}, +["Pristine Crystal Gemstone"]={150,155,157,160}, +["Pure Gold Ring"]={295,0,0,0}, +["Pure Shining Moonstone"]={175,180,182,185}, +["Purification Potion"]={285,300,320,340}, +["Quartz Halo"]={155,175,185,195}, +["Quickdraw Quiver"]={225,245,255,265}, +["Quicksilver Whirl"]={265,285,292,300}, +["Radiant Belt"]={260,280,290,300}, +["Radiant Boots"]={290,310,320,330}, +["Radiant Breastplate"]={270,290,300,310}, +["Radiant Circlet"]={295,315,325,335}, +["Radiant Ember Gemstone"]={200,205,207,210}, +["Radiant Gloves"]={285,305,315,325}, +["Radiant Leggings"]={300,320,330,340}, +["Radiant Thorium Twilight"]={255,270,277,285}, +["Rage Potion"]={60,90,110,130}, +["Rainbow Fin Albacore"]={50,90,110,130}, +["Raptor Hide Belt"]={165,185,195,205}, +["Raptor Hide Harness"]={165,185,195,205}, +["Red Dragonscale Boots"]={295,300,300,300}, +["Red Dragonscale Breastplate"]={300,320,330,340}, +["Red Dragonscale Leggings"]={295,300,300,300}, +["Red Dragonscale Shoulders"]={300,300,300,300}, +["Red Firework"]={150,150,162,175}, +["Red Linen Bag"]={70,95,112,130}, +["Red Linen Robe"]={40,65,82,100}, +["Red Linen Shirt"]={40,65,82,100}, +["Red Linen Vest"]={55,80,97,115}, +["Red Mageweave Bag"]={235,250,265,280}, +["Red Mageweave Gloves"]={225,240,255,270}, +["Red Mageweave Headband"]={240,255,270,285}, +["Red Mageweave Pants"]={215,230,245,260}, +["Red Mageweave Shoulders"]={235,250,265,280}, +["Red Mageweave Vest"]={215,230,245,260}, +["Red Rocket Cluster"]={225,225,237,250}, +["Red Swashbuckler's Shirt"]={175,185,190,195}, +["Red Whelp Gloves"]={120,145,157,170}, +["Red Woolen Bag"]={115,140,157,175}, +["Red Woolen Boots"]={95,120,137,155}, +["Redridge Goulash"]={100,135,155,175}, +["Refined Dwarven Necklace"]={185,185,200,215}, +["Refined Scale of Onyxia"]={300,0,0,0}, +["Reflective Breastplate"]={300,320,330,340}, +["Reflective Helmet"]={300,320,330,340}, +["Reflective Leggings"]={300,320,330,340}, +["Reflective Pauldrons"]={300,320,330,340}, +["Regal Twilight Staff"]={240,260,275,290}, +["Reinforced Linen Cape"]={60,85,102,120}, +["Reinforced Woolen Shoulders"]={120,145,162,180}, +["Resilient Arcane Gemstone"]={300,320,330,340}, +["Restorative Potion"]={210,225,245,265}, +["Resurged Topaz Gemstone"]={300,320,330,340}, +["Rich Purple Silk Shirt"]={185,195,200,205}, +["Ring of Midnight"]={125,145,155,165}, +["Ring of Purified Silver"]={135,155,165,175}, +["Ring of The Turtle"]={160,185,195,205}, +["Ring of Unleashed Potential"]={290,320,330,340}, +["Roast Raptor"]={175,215,235,255}, +["Roasted Boar Meat"]={1,45,65,85}, +["Roasted Kodo Meat"]={35,75,95,115}, +["Robe of Power"]={190,210,225,240}, +["Robe of Sacrifice"]={300,300,300,300}, +["Robe of Winter Night"]={285,300,315,330}, +["Robe of the Archmage"]={300,315,330,345}, +["Robe of the Void"]={300,315,330,345}, +["Robes of Arcana"]={150,170,185,200}, +["Rockscale Cod"]={175,190,210,230}, +["Rose Colored Goggles"]={230,250,260,270}, +["Rough Blasting Powder"]={1,20,30,40}, +["Rough Boomstick"]={50,80,95,110}, +["Rough Bronze Boots"]={95,125,140,155}, +["Rough Bronze Bracers"]={100,140,160,180}, +["Rough Bronze Cuirass"]={105,145,160,175}, +["Rough Bronze Leggings"]={105,145,160,175}, +["Rough Bronze Ring"]={90,100,107,115}, +["Rough Bronze Shoulders"]={110,140,155,170}, +["Rough Copper Bomb"]={30,60,75,90}, +["Rough Copper Ring"]={1,21,30,40}, +["Rough Copper Vest"]={1,15,35,55}, +["Rough Dynamite"]={1,30,45,60}, +["Rough Gemstone Cluster"]={35,45,50,55}, +["Rough Gold Ring"]={150,165,170,175}, +["Rough Grinding Stone"]={25,45,65,85}, +["Rough Gritted Paper"]={1,21,25,30}, +["Rough Iron Ring"]={150,165,170,175}, +["Rough Mithril Ring"]={175,175,177,180}, +["Rough Sharpening Stone"]={1,15,35,55}, +["Rough Silver Ring"]={125,135,145,155}, +["Rough Thorium Ring"]={235,245,250,255}, +["Rough Truesilver Ring"]={200,200,205,210}, +["Rough Weightstone"]={1,15,35,55}, +["Royal Gemstone Staff"]={215,235,245,255}, +["Ruby Ring of Ruin"]={300,330,350,370}, +["Rudeus' Focusing Cane"]={300,330,340,350}, +["Rugged Armor Kit"]={250,255,265,275}, +["Rugged Leather"]={250,250,250,250}, +["Rugged Leather Pants"]={35,65,80,95}, +["Rune Edge"]={285,310,322,335}, +["Rune-Etched Breastplate"]={300,320,330,340}, +["Rune-Etched Crown"]={300,320,330,340}, +["Rune-Etched Greaves"]={300,320,330,340}, +["Rune-Etched Grips"]={300,320,330,340}, +["Rune-Etched Legplates"]={300,320,330,340}, +["Rune-Etched Mantle"]={300,320,330,340}, +["Rune-Inscribed Plate Leggings"]={300,320,330,340}, +["Runebound Amulet"]={230,230,252,275}, +["Runecloth Bag"]={260,275,290,305}, +["Runecloth Bandage"]={260,260,290,320}, +["Runecloth Belt"]={255,270,285,300}, +["Runecloth Boots"]={280,295,310,325}, +["Runecloth Cloak"]={265,280,295,310}, +["Runecloth Gloves"]={275,290,305,320}, +["Runecloth Headband"]={295,310,325,340}, +["Runecloth Pants"]={285,300,315,330}, +["Runecloth Robe"]={260,275,290,305}, +["Runecloth Shoulders"]={300,315,330,345}, +["Runecloth Tunic"]={260,275,290,305}, +["Runed Arcanite Rod"]={290,310,330,350}, +["Runed Copper Belt"]={70,110,130,150}, +["Runed Copper Bracers"]={90,115,127,140}, +["Runed Copper Breastplate"]={80,120,140,160}, +["Runed Copper Gauntlets"]={40,80,100,120}, +["Runed Copper Pants"]={45,85,105,125}, +["Runed Copper Rod"]={1,5,7,10}, +["Runed Golden Rod"]={150,175,195,215}, +["Runed Mithril Hammer"]={245,270,282,295}, +["Runed Silver Rod"]={100,130,150,170}, +["Runed Stygian Belt"]={300,315,330,345}, +["Runed Stygian Boots"]={300,315,330,345}, +["Runed Stygian Leggings"]={300,315,330,345}, +["Runed Truesilver Ring"]={220,240,250,260}, +["Runed Truesilver Rod"]={200,220,240,260}, +["Runic Breastplate"]={300,320,330,340}, +["Runic Leather Armor"]={300,320,330,340}, +["Runic Leather Belt"]={280,300,310,320}, +["Runic Leather Bracers"]={275,295,305,315}, +["Runic Leather Gauntlets"]={270,290,300,310}, +["Runic Leather Headband"]={290,310,320,330}, +["Runic Leather Pants"]={300,320,330,340}, +["Runic Leather Shoulders"]={300,320,330,340}, +["Runic Plate Boots"]={300,320,330,340}, +["Runic Plate Helm"]={300,320,330,340}, +["Runic Plate Leggings"]={300,320,330,340}, +["Runic Plate Shoulders"]={300,320,330,340}, +["Runn Tum Tuber Surprise"]={275,315,335,355}, +["Sageblade"]={300,320,330,340}, +["Sagefish Delight"]={175,215,235,255}, +["Salt Shaker"]={250,270,280,290}, +["Sandstalker Bracers"]={300,320,330,340}, +["Sandstalker Breastplate"]={300,320,330,340}, +["Sandstalker Gauntlets"]={300,320,330,340}, +["Sapphire Luminescence"]={300,320,330,340}, +["Satchel of Cenarius"]={300,315,330,345}, +["Savory Deviate Delight"]={85,125,145,165}, +["Scorpid Surprise"]={20,60,80,100}, +["Searing Golden Blade"]={190,215,227,240}, +["Seasoned Wolf Kabob"]={100,140,160,180}, +["Serenity"]={285,310,322,335}, +["Serpent's Coil Staff"]={100,120,130,140}, +["Shadow Crescent Axe"]={200,225,237,250}, +["Shadow Goggles"]={120,145,157,170}, +["Shadow Hood"]={170,190,205,220}, +["Shadow Oil"]={165,190,210,230}, +["Shadow Protection Potion"]={135,160,180,200}, +["Shadoweave Boots"]={240,255,270,285}, +["Shadoweave Gloves"]={225,240,255,270}, +["Shadoweave Mask"]={245,260,275,290}, +["Shadoweave Pants"]={210,225,240,255}, +["Shadoweave Robe"]={215,230,245,260}, +["Shadoweave Shoulders"]={235,250,265,280}, +["Shadowfall Jewel"]={100,120,130,140}, +["Shadowforged Eye"]={85,105,115,125}, +["Shadowgem Band"]={105,125,135,145}, +["Shadowmoon Orb"]={85,105,115,125}, +["Shadowskin Boots"]={205,225,230,235}, +["Shadowskin Gloves"]={200,210,220,230}, +["Sharpened Citrine Gemstone"]={200,205,207,210}, +["Sharpened Claw"]={75,90,95,100}, +["Shifting Cloak"]={300,320,330,340}, +["Shimmering Aqua Gemstone"]={150,155,157,160}, +["Shimmering Bronze Ring"]={90,105,112,120}, +["Shimmering Diamond Band"]={300,320,330,340}, +["Shimmering Gold Necklace"]={190,205,215,225}, +["Shimmering Moonstone Tablet"]={195,195,217,240}, +["Shining Copper Cuffs"]={80,100,110,120}, +["Shining Sapphire Gemstone"]={250,0,0,0}, +["Shining Silver Breastplate"]={145,175,190,205}, +["Silk Bandage"]={150,150,180,210}, +["Silk Headband"]={160,170,175,180}, +["Silver Contact"]={90,110,125,140}, +["Silver Medallion"]={135,150,160,170}, +["Silver Rod"]={100,105,107,110}, +["Silver Skeleton Key"]={100,100,110,120}, +["Silver-plated Shotgun"]={130,155,167,180}, +["Silvered Bronze Boots"]={130,160,175,190}, +["Silvered Bronze Breastplate"]={130,160,175,190}, +["Silvered Bronze Gauntlets"]={135,165,180,195}, +["Silvered Bronze Leggings"]={155,180,192,205}, +["Silvered Bronze Shoulders"]={125,155,170,185}, +["Simple Black Dress"]={235,240,245,250}, +["Simple Dress"]={40,65,82,100}, +["Simple Kilt"]={75,100,117,135}, +["Simple Linen Boots"]={20,50,67,85}, +["Simple Linen Pants"]={1,35,47,60}, +["Skyfire Jewel"]={210,280,290,300}, +["Slitherskin Mackerel"]={1,45,65,85}, +["Small Blue Rocket"]={125,125,137,150}, +["Small Bronze Bomb"]={120,120,145,170}, +["Small Green Rocket"]={125,125,137,150}, +["Small Leather Ammo Pouch"]={30,60,75,90}, +["Small Pearl Ring"]={65,85,92,100}, +["Small Pearlstone Staff"]={55,75,82,90}, +["Small Red Rocket"]={125,125,137,150}, +["Small Seaforium Charge"]={100,130,145,160}, +["Small Silk Pack"]={150,170,185,200}, +["Smelt Bronze"]={65,65,90,115}, +["Smelt Copper"]={1,25,45,70}, +["Smelt Dark Iron"]={230,230,230,230}, +["Smelt Dreamsteel"]={300,350,362,375}, +["Smelt Elementium"]={300,350,362,375}, +["Smelt Gold"]={155,170,177,185}, +["Smelt Iron"]={125,125,130,140}, +["Smelt Mithril"]={175,175,175,175}, +["Smelt Silver"]={75,100,112,125}, +["Smelt Steel"]={165,165,165,165}, +["Smelt Thorium"]={250,250,250,250}, +["Smelt Tin"]={65,65,70,75}, +["Smelt Truesilver"]={230,230,230,230}, +["Smoked Bear Meat"]={40,80,100,120}, +["Smoked Desert Dumplings"]={285,325,345,365}, +["Smoked Sagefish"]={80,120,140,160}, +["Smoking Heart of the Mountain"]={265,285,305,325}, +["Smoldering Brooch"]={200,220,230,240}, +["Snake Burst Firework"]={250,250,260,270}, +["Sniper Scope"]={240,260,270,280}, +["SnowMaster 9000"]={190,0,0,0}, +["Soft-soled Linen Boots"]={80,105,122,140}, +["Softglow Ring"]={65,85,92,100}, +["Solid Blasting Powder"]={175,175,185,195}, +["Solid Dynamite"]={175,175,185,195}, +["Solid Gemstone Cluster"]={200,200,205,210}, +["Solid Grinding Stone"]={200,200,205,210}, +["Solid Gritted Paper"]={200,200,205,210}, +["Solid Iron Maul"]={155,180,192,205}, +["Solid Sharpening Stone"]={200,200,205,210}, +["Solid Weightstone"]={200,200,205,210}, +["Soothing Turtle Bisque"]={175,215,235,255}, +["Soul Pouch"]={260,275,290,305}, +["South Seas Pirate Disguise"]={1,0,0,0}, +["Specter's Shade Ring"]={180,0,0,0}, +["Spellpower Goggles Xtreme"]={225,245,255,265}, +["Spellpower Goggles Xtreme Plus"]={270,290,300,310}, +["Spellweaver Pendant"]={265,275,285,295}, +["Spellweaver Rod"]={265,285,292,300}, +["Spellwoven Nobility Drape"]={300,0,0,0}, +["Sphinx's Wisdom Staff"]={170,190,200,210}, +["Spiced Chili Crab"]={225,265,285,305}, +["Spiced Wolf Meat"]={10,50,70,90}, +["Spider Belt"]={180,200,215,230}, +["Spider Sausage"]={200,240,260,280}, +["Spider Silk Slippers"]={140,160,175,190}, +["Spidersilk Boots"]={125,150,167,185}, +["Spire of Channeled Power"]={300,325,335,345}, +["Spitfire Bracers"]={300,320,330,340}, +["Spitfire Breastplate"]={300,320,330,340}, +["Spitfire Gauntlets"]={300,320,330,340}, +["Spotted Yellowtail"]={225,265,285,305}, +["Staff of Blossomed Jade"]={165,185,195,205}, +["Staff of Gallitrea"]={200,225,235,245}, +["Standard Scope"]={110,135,147,160}, +["Star Belt"]={200,220,235,250}, +["Starforge Amulet"]={220,220,240,260}, +["Starry Thorium Band"]={260,260,275,290}, +["Steel Belt Buckle"]={200,195,200,205}, +["Steel Breastplate"]={200,225,237,250}, +["Steel Plate Armor"]={225,225,225,225}, +["Steel Plate Barbute"]={230,230,230,230}, +["Steel Plate Boots"]={220,220,220,220}, +["Steel Plate Gauntlets"]={220,220,220,220}, +["Steel Plate Helm"]={215,235,245,255}, +["Steel Plate Legguards"]={225,225,225,225}, +["Steel Plate Pauldrons"]={230,230,230,230}, +["Steel Weapon Chain"]={190,215,227,240}, +["Stellar Gemguards"]={270,0,0,0}, +["Stellar Ruby Ring"]={300,0,0,0}, +["Stonescale Oil"]={250,250,255,260}, +["Stonesplinter Trogg Disguise"]={1,0,0,0}, +["Storm Gauntlets"]={295,315,325,335}, +["Stormcloth Boots"]={250,265,280,295}, +["Stormcloth Gloves"]={220,235,250,265}, +["Stormcloth Headband"]={240,255,270,285}, +["Stormcloth Pants"]={220,235,250,265}, +["Stormcloth Shoulders"]={245,260,275,290}, +["Stormcloth Vest"]={225,240,255,270}, +["Stormcloud Shackles"]={300,0,0,0}, +["Stormcloud Sigil"]={290,310,325,340}, +["Stormcloud Signet"]={290,310,320,330}, +["Stormreaver Gloves"]={185,0,0,0}, +["Stormscale Leggings"]={300,300,300,300}, +["Stormshroud Armor"]={285,305,315,325}, +["Stormshroud Gloves"]={300,320,330,340}, +["Stormshroud Pants"]={275,295,305,315}, +["Stormshroud Shoulders"]={295,315,325,335}, +["Strider Stew"]={50,90,110,130}, +["Strong Anti-Venom"]={130,130,165,200}, +["Strong Troll's Blood Potion"]={125,150,170,190}, +["Stronghold Gauntlets"]={300,320,330,340}, +["Stunning Imperial Gemstone"]={300,310,315,320}, +["Sturdy Copper Ring"]={25,45,52,60}, +["Stylish Blue Shirt"]={120,145,162,180}, +["Stylish Green Shirt"]={120,145,162,180}, +["Stylish Red Shirt"]={110,135,152,170}, +["Succulent Pork Ribs"]={110,130,150,170}, +["Sulfuron Hammer"]={300,325,337,350}, +["Sunburst Tiara"]={250,250,270,290}, +["Superior Healing Potion"]={215,230,250,270}, +["Superior Mana Potion"]={260,275,295,315}, +["Swift Boots"]={200,220,230,240}, +["Swift Flight Bracers"]={300,320,330,340}, +["Swiftness Potion"]={60,90,110,130}, +["Swim Speed Potion"]={100,130,150,170}, +["Sylvan Crown"]={300,315,330,345}, +["Sylvan Shoulders"]={300,315,330,345}, +["Sylvan Vest"]={300,315,330,345}, +["Syndicate Disguise"]={1,0,0,0}, +["Talisman of Hinderance"]={300,320,330,340}, +["Talisman of Stone"]={100,125,135,145}, +["Target Dummy"]={85,115,130,145}, +["Tasty Lion Steak"]={150,190,210,230}, +["Tempered Azerothian Gemstone"]={275,280,282,285}, +["Tender Wolf Steak"]={225,265,285,305}, +["The Big One"]={235,235,255,275}, +["The Golden Goblet"]={175,195,202,210}, +["The King's Conviction"]={85,105,115,125}, +["The Mortar: Reloaded"]={205,205,205,205}, +["The Shatterer"]={235,260,272,285}, +["Thick Armor Kit"]={200,220,230,240}, +["Thick Leather"]={200,200,202,205}, +["Thick Leather Ammo Pouch"]={225,245,255,265}, +["Thick Murloc Armor"]={170,190,200,210}, +["Thick Obsidian Breastplate"]={300,320,330,340}, +["Thick War Axe"]={70,110,130,150}, +["Thistle Tea"]={60,100,120,140}, +["Thorium Armor"]={250,270,280,290}, +["Thorium Belt"]={250,270,280,290}, +["Thorium Belt Buckle"]={240,265,277,290}, +["Thorium Boots"]={280,300,310,320}, +["Thorium Bracers"]={255,275,285,295}, +["Thorium Greatsword"]={260,285,297,310}, +["Thorium Grenade"]={260,280,290,300}, +["Thorium Helm"]={280,300,310,320}, +["Thorium Leggings"]={300,320,330,340}, +["Thorium Rifle"]={260,280,290,300}, +["Thorium Shells"]={285,305,315,325}, +["Thorium Shield Spike"]={275,295,305,315}, +["Thorium Spurs"]={275,300,310,320}, +["Thorium Tube"]={275,295,305,315}, +["Thorium Widget"]={260,280,290,300}, +["Tigercrest Ring"]={50,70,77,85}, +["Timbermaw Brawlers"]={300,320,330,340}, +["Titanic Leggings"]={300,320,330,340}, +["Topaz Studded Ring"]={70,90,95,100}, +["Totem of Self Preservation"]={135,155,165,175}, +["Tough Scorpid Boots"]={235,255,265,275}, +["Tough Scorpid Bracers"]={220,240,250,260}, +["Tough Scorpid Breastplate"]={220,240,250,260}, +["Tough Scorpid Gloves"]={225,245,255,265}, +["Tough Scorpid Helm"]={250,270,280,290}, +["Tough Scorpid Leggings"]={245,265,275,285}, +["Tough Scorpid Shoulders"]={240,260,270,280}, +["Toughened Leather Armor"]={120,145,157,170}, +["Toughened Leather Gloves"]={135,160,172,185}, +["Towerforge Breastplate"]={300,325,337,350}, +["Towerforge Crown"]={300,325,337,350}, +["Towerforge Demolisher"]={300,325,337,350}, +["Towerforge Pauldrons"]={300,325,337,350}, +["Tranquil Mechanical Yeti"]={250,320,330,340}, +["Transmute: Air to Fire"]={275,275,282,290}, +["Transmute: Arcanite"]={275,275,282,290}, +["Transmute: Earth to Life"]={275,275,282,290}, +["Transmute: Earth to Water"]={275,275,282,290}, +["Transmute: Elemental Earth"]={300,315,322,330}, +["Transmute: Elemental Fire"]={300,301,305,310}, +["Transmute: Elemental Water"]={300,315,322,330}, +["Transmute: Fire to Earth"]={275,275,282,290}, +["Transmute: Iron to Gold"]={225,240,260,280}, +["Transmute: Life to Earth"]={275,275,282,290}, +["Transmute: Mithril to Truesilver"]={225,240,260,280}, +["Transmute: Undeath to Water"]={275,275,282,290}, +["Transmute: Water to Air"]={275,275,282,290}, +["Transmute: Water to Undeath"]={275,275,282,290}, +["Truefaith Gloves"]={150,170,185,200}, +["Truefaith Vestments"]={300,315,330,345}, +["Truesilver Belt Buckle"]={225,225,230,235}, +["Truesilver Breastplate"]={245,265,275,285}, +["Truesilver Champion"]={260,285,297,310}, +["Truesilver Gauntlets"]={225,245,255,265}, +["Truesilver Rod"]={200,205,207,210}, +["Truesilver Skeleton Key"]={200,200,210,220}, +["Truesilver Transformer"]={260,270,275,280}, +["Turtle Scale Bracers"]={210,230,240,250}, +["Turtle Scale Breastplate"]={210,230,240,250}, +["Turtle Scale Gloves"]={205,225,235,245}, +["Turtle Scale Helm"]={230,250,260,270}, +["Turtle Scale Leggings"]={235,255,265,275}, +["Tuxedo Jacket"]={250,265,280,295}, +["Tuxedo Pants"]={245,250,255,260}, +["Tuxedo Shirt"]={240,245,250,255}, +["Twilight Opal Cascade"]={300,0,0,0}, +["Ultra-Flash Shadow Reflector"]={300,320,330,340}, +["Ultrasafe Transporter - Gadgetzan"]={260,0,0,0}, +["Undermine Clam Chowder"]={225,265,285,305}, +["Unstable Arcane Gemstone"]={275,0,0,0}, +["Unstable Mining Dynamite"]={75,90,97,105}, +["Unstable Trigger"]={200,200,220,240}, +["Untempered Runeblade"]={300,320,330,340}, +["Venomspire Diadem"]={145,165,175,185}, +["Verdant Dreamer's Breastplate"]={300,300,300,300}, +["Vitriol Brooch"]={205,220,227,235}, +["Voice Amplification Modulator"]={290,310,320,330}, +["Voidheart Charm"]={280,280,300,320}, +["Volatile Concoction"]={75,0,0,0}, +["Volcanic Breastplate"]={285,305,315,325}, +["Volcanic Hammer"]={290,315,327,340}, +["Volcanic Leggings"]={270,290,300,310}, +["Volcanic Shoulders"]={300,320,330,340}, +["Voltage-Neutralizing Nature Reflector"]={290,310,320,330}, +["Warbear Harness"]={275,295,305,315}, +["Warbear Woolies"]={285,305,315,325}, +["Weak Troll's Blood Potion"]={15,60,80,100}, +["Westfall Stew"]={75,115,135,155}, +["Whirring Bronze Gizmo"]={125,125,150,175}, +["White Bandit Mask"]={215,220,225,230}, +["White Leather Jerkin"]={60,90,105,120}, +["White Linen Robe"]={30,55,72,90}, +["White Linen Shirt"]={1,35,47,60}, +["White Swashbuckler's Shirt"]={160,170,175,180}, +["White Wedding Dress"]={250,255,260,265}, +["White Woolen Dress"]={110,135,152,170}, +["Whitesoul Helm"]={300,320,330,340}, +["Wicked Leather Armor"]={300,320,330,340}, +["Wicked Leather Belt"]={300,320,330,340}, +["Wicked Leather Bracers"]={265,285,295,305}, +["Wicked Leather Gauntlets"]={260,280,290,300}, +["Wicked Leather Headband"]={280,300,310,320}, +["Wicked Leather Pants"]={290,310,320,330}, +["Wicked Mithril Blade"]={225,250,262,275}, +["Wild Leather Boots"]={245,265,275,285}, +["Wild Leather Cloak"]={250,270,280,290}, +["Wild Leather Helmet"]={225,245,255,265}, +["Wild Leather Leggings"]={250,270,280,290}, +["Wild Leather Shoulders"]={220,240,250,260}, +["Wild Leather Vest"]={225,245,255,265}, +["Wildthorn Mail"]={270,290,300,310}, +["Wildvine Potion"]={225,240,260,280}, +["Windbinder Gloves"]={300,315,330,345}, +["Windwalker Boots"]={300,300,300,300}, +["Wisdom of the Timbermaw"]={290,305,320,335}, +["Wizard Oil"]={275,285,295,305}, +["Wizardweave Leggings"]={275,290,305,320}, +["Wizardweave Robe"]={300,315,330,345}, +["Wizardweave Turban"]={300,315,330,345}, +["Wolfshead Helm"]={225,245,255,265}, +["Wool Bandage"]={80,80,115,150}, +["Woolen Bag"]={80,105,122,140}, +["Woolen Boots"]={95,120,137,155}, +["Woolen Cape"]={75,100,117,135}, +["World Enlarger"]={260,260,265,270}, +["Wound Poison"]={1,185,210,235}, +["Wound Poison II"]={180,0,0,0}, +["Wound Poison III"]={220,0,0,0}, +["Wound Poison IV"]={260,0,0,0}, +["一把螺栓"]={30,45,52,60}, +["不牢固的扳机"]={200,200,220,240}, +["不稳定的奥术宝石"]={275,0,0,0}, +["不稳定的混合物"]={75,0,0,0}, +["不稳定的采矿炸药"]={75,90,97,105}, +["世界放大器"]={260,260,265,270}, +["丛林大杂烩"]={175,215,235,255}, +["丝绸卷"]={125,135,140,145}, +["丝绸小包"]={150,170,185,200}, +["丝质头带"]={160,170,175,180}, +["丝质绷带"]={150,150,180,210}, +["丝质长披风"]={185,205,220,235}, +["中型护甲片"]={100,115,122,130}, +["中皮"]={100,100,105,110}, +["乌木刀"]={255,280,292,305}, +["乌木戒指"]={75,95,105,115}, +["乌龟戒指"]={160,185,195,205}, +["亚麻包"]={45,70,87,105}, +["亚麻布卷"]={1,25,37,50}, +["亚麻披风"]={1,35,47,60}, +["亚麻绷带"]={1,30,45,60}, +["亚麻腰带"]={15,50,67,85}, +["亚麻靴"]={65,90,107,125}, +["亮布手套"]={270,285,300,315}, +["亮布披风"]={275,290,305,320}, +["亮布短裤"]={290,305,320,335}, +["亮布长袍"]={270,285,300,315}, +["亮铜项链"]={65,85,92,100}, +["亮闪闪的钢匕首"]={180,205,217,230}, +["仇恨熔炉头盔"]={290,300,310,320}, +["仇恨熔炉护手"]={285,300,310,320}, +["仇恨熔炉护腿"]={290,300,310,320}, +["仇恨熔炉胸甲"]={290,300,310,320}, +["仇恨熔炉腰带"]={275,300,312,325}, +["仇恨熔炉靴子"]={275,300,310,320}, +["仿真机械蛙"]={265,285,295,305}, +["优质治疗药水"]={215,230,250,270}, +["优质法力药水"]={260,275,295,315}, +["优质皮外套"]={85,115,130,145}, +["优质皮带"]={80,110,125,140}, +["优质皮手套"]={75,105,120,135}, +["优质皮披风"]={85,105,120,135}, +["优质皮裤"]={105,130,142,155}, +["优质皮靴"]={90,120,135,150}, +["优雅翡翠"]={250,0,0,0}, +["体面的白衬衣"]={170,180,185,190}, +["侏儒作战小鸡"]={230,250,260,270}, +["侏儒微调器"]={175,175,195,215}, +["侏儒护目镜"]={210,230,240,250}, +["侏儒撒网器"]={210,230,240,250}, +["侏儒死亡射线"]={240,260,270,280}, +["侏儒洗脑帽"]={235,255,265,275}, +["侏儒火箭靴"]={225,245,255,265}, +["侏儒缩小射线"]={205,225,235,245}, +["侏儒通用遥控器"]={125,150,162,175}, +["侏儒防护腰带"]={215,235,245,255}, +["侏儒隐形装置"]={200,220,230,240}, +["侦测亡灵药剂"]={230,245,265,285}, +["侦测恶魔药剂"]={250,265,285,305}, +["侦测次级隐形药剂"]={195,215,235,255}, +["便携式虫洞发生器 - 奥格瑞玛"]={125,0,0,0}, +["便携式虫洞发生器 - 暴风城"]={125,0,0,0}, +["便携式青铜迫击炮"]={165,185,195,205}, +["信念外衣"]={300,315,330,345}, +["信念手套"]={150,170,185,200}, +["修理机器人74A型"]={300,320,330,340}, +["修道院灰烬护腕"]={170,185,195,205}, +["偏斜护肩"]={300,315,322,330}, +["元素磨刀石"]={300,300,310,320}, +["充能奥妮克希亚鳞片"]={300,0,0,0}, +["先知之刃"]={300,320,330,340}, +["先知药剂"]={265,280,300,320}, +["光亮护腕"]={300,320,330,340}, +["光芒之怒指环"]={300,320,330,340}, +["光芒手套"]={300,315,330,345}, +["光芒护腕"]={300,315,330,345}, +["光芒护腿"]={300,315,330,345}, +["光芒衬肩"]={300,315,330,345}, +["光芒长袍"]={300,315,330,345}, +["光辉蓝宝石戒指"]={300,320,330,340}, +["克劳福德苹果挞"]={175,0,0,0}, +["免伤锁甲"]={300,320,330,340}, +["公式:附魔护腕 - 敏捷"]={185,0,0,0}, +["兽人护腿"]={210,250,260,270}, +["农夫伪装"]={1,0,0,0}, +["冥想手套"]={130,150,165,180}, +["冬夜法袍"]={285,300,315,330}, +["冬天爷爷的手套"]={190,210,220,230}, +["冰川外衣"]={300,315,330,345}, +["冰川手套"]={300,315,330,345}, +["冰川护腕"]={300,315,330,345}, +["冰川披风"]={300,315,330,345}, +["冰覆披风"]={200,220,235,250}, +["冰霜之力药剂"]={190,210,230,250}, +["冰霜之油"]={200,220,240,260}, +["冰霜皮质披风"]={180,200,210,220}, +["冰霜缚斩者"]={180,190,195,200}, +["冰霜防护药水"]={190,205,225,245}, +["冷木斧"]={270,290,300,310}, +["净化药水"]={285,300,320,340}, +["凤凰手套"]={125,150,167,185}, +["凤凰短裤"]={125,150,167,185}, +["初级坚韧药剂"]={50,80,100,120}, +["初级巫师之油"]={45,0,0,0}, +["初级抗魔药水"]={110,135,155,175}, +["初级敏捷药剂"]={50,80,100,120}, +["初级治疗药水"]={1,55,75,95}, +["初级法力之油"]={150,0,0,0}, +["初级法力药水"]={25,65,85,105}, +["初级活力药水"]={40,70,90,110}, +["初级防御药剂"]={1,55,75,95}, +["刺花护腕"]={105,125,135,145}, +["刺须鲶鱼"]={100,140,160,180}, +["力反馈盾牌"]={300,320,330,340}, +["力量法袍"]={190,210,225,240}, +["加列拉之杖"]={200,225,235,245}, +["加速药水"]={300,315,322,330}, +["劣质平衡石"]={1,15,35,55}, +["劣质火枪"]={50,80,95,110}, +["劣质火药"]={1,20,30,40}, +["劣质炸药"]={1,30,45,60}, +["劣质瑟银戒指"]={125,135,145,155}, +["劣质的宝石串"]={35,45,50,55}, +["劣质的铜戒指"]={1,21,30,40}, +["劣质真银戒指"]={200,200,205,210}, +["劣质砂纸"]={1,21,25,30}, +["劣质砂轮"]={25,45,65,85}, +["劣质磨刀石"]={1,15,35,55}, +["劣质秘银戒指"]={175,175,177,180}, +["劣质铜壳炸弹"]={30,60,75,90}, +["劣质铜外衣"]={1,15,35,55}, +["劣质青铜战靴"]={95,125,140,155}, +["劣质青铜护肩"]={110,140,155,170}, +["劣质青铜护腕"]={100,140,160,180}, +["劣质青铜护腿"]={105,145,160,175}, +["劣质青铜胸甲"]={105,145,160,175}, +["化石合剂"]={300,315,322,330}, +["北极外套"]={300,320,330,340}, +["北极手套"]={300,320,330,340}, +["北极护腕"]={300,320,330,340}, +["午夜之戒"]={125,145,155,165}, +["午夜吊坠"]={120,145,155,165}, +["半人马战甲"]={300,320,330,340}, +["半人马指环"]={160,180,190,200}, +["半人马权威护肩"]={300,320,330,340}, +["华丽山地宝石"]={300,330,345,360}, +["华丽瑟银手斧"]={275,300,312,325}, +["华丽的秘银权杖"]={200,220,230,240}, +["华丽秘银手镯"]={200,225,237,250}, +["华丽血石匕首"]={300,320,330,340}, +["卓越巫师之油"]={300,0,0,0}, +["卓越法力之油"]={300,0,0,0}, +["南海海盗伪装"]={1,0,0,0}, +["占卜者兜帽"]={230,230,230,230}, +["占卜者手套"]={225,225,225,225}, +["占卜者护肩"]={230,230,230,230}, +["占卜者裤子"]={230,230,230,230}, +["占卜者长袍"]={235,235,235,235}, +["占卜者鞋子"]={225,225,225,225}, +["卡多雷蜘蛛烤肉"]={10,50,70,90}, +["厚丝质绷带"]={180,180,210,240}, +["厚亚麻绷带"]={40,50,75,100}, +["厚皮"]={200,200,202,205}, +["厚皮弹药包"]={225,245,255,265}, +["厚符文布绷带"]={290,290,320,350}, +["厚绒线绷带"]={115,115,150,185}, +["厚重战斧"]={70,110,130,150}, +["厚重护甲片"]={200,220,230,240}, +["厚重的宝石串"]={150,150,152,155}, +["厚重砂纸"]={150,150,155,160}, +["厚重黑曜石胸甲"]={300,320,330,340}, +["厚魔纹绷带"]={240,240,270,300}, +["厚鱼人皮甲"]={170,190,200,210}, +["原始水晶石"]={150,155,157,160}, +["原始蝙蝠皮外套"]={300,320,330,340}, +["原始蝙蝠皮手套"]={300,320,330,340}, +["原始蝙蝠皮护腕"]={300,320,330,340}, +["双线毛纺护肩"]={110,135,152,170}, +["发条娃娃"]={205,205,205,205}, +["发条式同步协调陀螺仪"]={170,170,190,210}, +["古代法老智慧之杖"]={170,190,200,210}, +["古代矮人宝石"]={185,185,195,205}, +["古拉巴什浓汤"]={300,300,300,300}, +["古拉巴什魔精"]={300,315,322,330}, +["吉尔尼斯热炖菜"]={200,240,260,280}, +["君王板甲头盔"]={295,315,325,335}, +["君王板甲战靴"]={295,315,325,335}, +["君王板甲护肩"]={265,285,295,305}, +["君王板甲护胸"]={300,320,330,340}, +["君王板甲护腕"]={270,290,300,310}, +["君王板甲护腿"]={300,320,330,340}, +["君王板甲腰带"]={265,285,295,305}, +["啤酒烤猪排"]={25,60,80,100}, +["图样:暴掠手套"]={185,0,0,0}, +["圆润翡翠"]={175,180,182,185}, +["土灵丝质腰带"]={195,215,230,245}, +["土灵外衣"]={170,190,205,220}, +["土灵皮护肩"]={135,160,172,185}, +["地狱火手套"]={300,315,330,345}, +["地狱灾祸手杖"]={160,180,190,200}, +["地精工兵炸药"]={205,205,225,245}, +["地精施工头盔"]={205,225,235,245}, +["地精无线电"]={225,240,250,260}, +["地精暗雷"]={195,215,225,235}, +["地精火箭头盔"]={245,265,275,285}, +["地精火箭燃油"]={210,225,245,265}, +["地精火箭燃油配方"]={205,205,205,205}, +["地精火箭靴"]={225,245,255,265}, +["地精炸弹箱"]={230,230,250,270}, +["地精芥末蘸蚌肉"]={125,165,185,205}, +["地精起搏器"]={165,165,180,200}, +["地精起搏器XL型"]={265,285,295,305}, +["地精迫击炮"]={205,225,235,245}, +["地精采矿头盔"]={205,225,235,245}, +["地精龙枪"]={240,260,270,280}, +["坚固的平衡石"]={200,200,205,210}, +["坚固的砂轮"]={200,200,205,210}, +["坚固的磨刀石"]={200,200,205,210}, +["坚固砂纸"]={200,200,205,210}, +["坚硬的铜戒指"]={25,45,52,60}, +["坚韧药剂"]={175,195,215,235}, +["塔铸皇冠"]={300,325,337,350}, +["塔铸破坏者"]={300,325,337,350}, +["塔铸肩铠"]={300,325,337,350}, +["塔铸胸甲"]={300,325,337,350}, +["塞纳留斯之袋"]={300,315,330,345}, +["塞纳里奥草药包"]={275,290,305,320}, +["增亮护目镜"]={175,195,205,215}, +["增长药剂"]={90,120,140,160}, +["多彩护手"]={300,320,330,340}, +["多彩护腿"]={300,300,300,300}, +["多彩披风"]={300,320,330,340}, +["多彩褶裙"]={120,145,162,180}, +["多汁猪排"]={110,130,150,170}, +["多重抗性合剂"]={300,315,322,330}, +["夜幕"]={300,320,330,340}, +["夜色外套"]={205,225,235,245}, +["夜色头带"]={205,225,235,245}, +["夜色护肩"]={210,230,240,250}, +["夜色披风"]={230,250,260,270}, +["夜色短裤"]={230,250,260,270}, +["夜色长靴"]={235,255,265,275}, +["夜视步枪"]={145,170,182,195}, +["夜鳞鱼汤"]={250,290,310,330}, +["大口径秘银步枪"]={220,240,250,260}, +["大地卫士外套"]={300,300,300,300}, +["大地胸甲"]={265,0,0,0}, +["大块的熊排"]={110,150,170,190}, +["大型爆盐炸弹"]={200,200,220,240}, +["大型红色烟花"]={175,175,187,200}, +["大型红色烟花束"]={275,275,280,285}, +["大型绿色烟花"]={175,175,187,200}, +["大型绿色烟花束"]={275,275,280,285}, +["大型蓝色烟花"]={175,175,187,200}, +["大型蓝色烟花束"]={275,275,280,285}, +["大型铜壳炸弹"]={105,105,130,155}, +["大师长袍"]={115,140,157,175}, +["大法师之袍"]={300,315,330,345}, +["大法师腰带"]={300,315,330,345}, +["大炸弹"]={235,235,255,275}, +["大酋长头盔"]={300,320,330,340}, +["大附魔袋"]={300,315,330,345}, +["大鱼片"]={275,315,335,355}, +["天火宝石戒指"]={210,280,290,300}, +["天蓝星火"]={265,265,285,305}, +["天青石胸针"]={205,220,227,235}, +["奇美拉外衣"]={290,310,320,330}, +["奇美拉手套"]={265,270,280,290}, +["奇美拉护腿"]={280,300,310,320}, +["奇美拉长靴"]={275,295,305,315}, +["奢华宝石项链"]={80,100,110,120}, +["奥妮克希亚鳞片披风"]={300,320,330,340}, +["奥妮克希亚鳞片胸甲"]={300,0,0,0}, +["奥术光辉吊坠"]={280,300,305,310}, +["奥术炸弹"]={300,320,330,340}, +["奥术翡翠"]={295,300,302,305}, +["奥法之袍"]={150,170,185,200}, +["奥法巨人药剂"]={300,310,330,350}, +["奥法药剂"]={235,250,270,290}, +["奥秘之杖"]={300,320,330,340}, +["奥金万能钥匙"]={275,275,280,285}, +["奥金圣剑"]={300,320,330,340}, +["奥金带扣"]={300,325,337,350}, +["奥金幼龙"]={300,320,330,340}, +["奥金斧"]={300,320,330,340}, +["奥金棒"]={275,275,280,285}, +["姜饼"]={1,45,65,85}, +["嫩狼肉排"]={225,265,285,305}, +["孔雀石戒指"]={20,40,47,55}, +["宇宙外衣"]={300,300,300,300}, +["宇宙头饰"]={300,300,300,300}, +["宇宙护腿"]={300,300,300,300}, +["宇宙衬肩"]={300,300,300,300}, +["守御护符"]={300,320,330,340}, +["守护之甲"]={175,195,205,215}, +["守护图腾"]={135,155,165,175}, +["守护手套"]={190,210,220,230}, +["守护披风"]={185,205,215,225}, +["守护短裤"]={160,180,190,200}, +["守护腕甲"]={195,215,225,235}, +["守护腰带"]={170,190,200,210}, +["安全传送器 - 加基森"]={260,0,0,0}, +["安德麦蚌肉杂烩"]={225,265,285,305}, +["安静的机械雪人"]={250,320,330,340}, +["完美的奥金步枪"]={300,320,330,340}, +["宝石商的魔棒"]={235,0,0,0}, +["宝石皮带"]={185,205,215,225}, +["宝石瞄准镜"]={225,140,147,155}, +["宝石纲要"]={275,275,297,320}, +["宝石透镜"]={125,140,147,155}, +["宝石项圈"]={160,180,190,200}, +["实心炸弹"]={175,175,185,195}, +["实心炸药"]={175,175,185,195}, +["寒冬之刃"]={190,215,227,240}, +["寒冰偏斜器"]={155,175,185,195}, +["寒冰护卫者"]={300,320,330,340}, +["寒鳞护手"]={300,320,330,340}, +["寒鳞护腕"]={300,320,330,340}, +["寒鳞胸甲"]={300,320,330,340}, +["寻心者"]={300,320,330,340}, +["导尘者腰带"]={300,315,330,345}, +["封印雕饰"]={245,245,265,285}, +["将军之帽"]={240,255,270,285}, +["小型巨魔血戒"]={50,70,80,90}, +["小型强固戒指"]={50,70,77,85}, +["小型爆盐炸弹"]={100,130,145,160}, +["小型珍珠戒指"]={65,85,92,100}, +["小型红色烟花"]={125,125,137,150}, +["小型绿色烟花"]={125,125,137,150}, +["小型蓝色烟花"]={125,125,137,150}, +["小型青铜炸弹"]={120,120,145,170}, +["小珍珠法杖"]={55,75,82,90}, +["尖锐黄水晶"]={200,205,207,210}, +["屠魔药剂"]={250,265,285,305}, +["山地护肩"]={130,155,167,180}, +["山地披风"]={150,170,180,190}, +["山地皮外衣"]={100,125,137,150}, +["山地皮手套"]={145,170,182,195}, +["山地腰带"]={120,145,157,170}, +["山狗肉排"]={50,90,110,130}, +["山猫步靴"]={75,0,0,0}, +["岩石戒指"]={100,120,130,140}, +["崇高领主甲胄"]={300,320,330,340}, +["工匠眼镜"]={185,205,215,225}, +["巧克力鱼"]={300,300,300,300}, +["巨人药剂"]={245,260,280,300}, +["巨型宝石指环"]={300,0,0,0}, +["巨型瑟银战斧"]={280,305,317,330}, +["巨型铁斧"]={185,210,222,235}, +["巨型铁锤"]={145,175,190,205}, +["巨型黑色锤"]={230,255,267,280}, +["巫师之油"]={275,0,0,0}, +["巫术师兜帽"]={165,185,200,215}, +["巫毒披风"]={240,260,270,280}, +["巫毒短裤"]={240,260,270,280}, +["巫毒长袍"]={215,235,245,255}, +["巫毒面具"]={220,240,250,260}, +["巫纹头巾"]={300,315,330,345}, +["巫纹护腿"]={275,290,305,320}, +["巫纹长袍"]={300,315,330,345}, +["帝国板甲护手"]={270,280,285,290}, +["干烤狼肉串"]={100,140,160,180}, +["平静"]={285,310,322,335}, +["幻光宝玉"]={200,205,205,205}, +["幻彩吊坠"]={300,300,307,315}, +["幻影之刃"]={245,270,282,295}, +["幻象染料"]={245,260,280,300}, +["幽影墨玉"]={260,265,267,270}, +["幽灵戒指"]={180,0,0,0}, +["弱效巨魔之血药水"]={15,60,80,100}, +["强力净化器"]={275,285,290,295}, +["强力嗅盐"]={300,300,330,360}, +["强力巨魔之血药水"]={125,150,170,190}, +["强力抗毒药剂"]={130,130,165,200}, +["强力爆盐炸弹"]={275,275,285,295}, +["强化亚麻斗篷"]={60,85,102,120}, +["强化毛纺护肩"]={120,145,162,180}, +["强效冰霜之力药剂"]={300,300,315,330}, +["强效冰霜防护药水"]={290,305,325,345}, +["强效奥术之力药剂"]={300,300,315,330}, +["强效奥术防护药水"]={290,305,325,345}, +["强效奥法药剂"]={285,300,320,340}, +["强效怒气药水"]={255,270,290,310}, +["强效敏捷药剂"]={240,255,275,295}, +["强效昏睡药水"]={275,290,310,330}, +["强效暗影防护药水"]={290,305,325,345}, +["强效水下呼吸药剂"]={215,230,250,270}, +["强效治疗药水"]={155,175,195,215}, +["强效法力药水"]={205,220,240,260}, +["强效火力药剂"]={250,265,285,305}, +["强效火焰防护药水"]={290,305,325,345}, +["强效石盾药水"]={280,295,315,335}, +["强效神圣防护药水"]={290,305,325,345}, +["强效禁锢徽记"]={210,230,240,250}, +["强效秘法杖"]={175,0,0,0}, +["强效聪颖药剂"]={235,250,270,290}, +["强效自然力量药剂"]={300,315,322,330}, +["强效自然防护药水"]={290,305,325,345}, +["强效防御药剂"]={195,215,235,255}, +["强效魔法杖"]={70,0,0,0}, +["强能草药沙拉"]={300,325,345,365}, +["强能黄水晶吊坠"]={175,195,202,210}, +["彩虹之锤"]={140,170,185,200}, +["彩鳍鱼"]={50,90,110,130}, +["影月宝珠"]={85,105,115,125}, +["影皮手套"]={200,210,220,230}, +["影铸之眼"]={85,105,115,125}, +["微光白玉坠戒"]={300,0,0,0}, +["德莱尼水晶魔棒"]={200,225,235,245}, +["忘却的圣杯"]={300,330,340,350}, +["快捷箭袋"]={225,245,255,265}, +["快速暗影反射器"]={300,320,330,340}, +["怒气药水"]={60,90,110,130}, +["急速生长药剂"]={200,200,212,225}, +["恶魔布包"]={280,300,315,330}, +["恶魔布帽"]={290,305,320,335}, +["恶魔布手套"]={300,315,330,345}, +["恶魔布护肩"]={300,315,330,345}, +["恶魔布短裤"]={275,290,305,320}, +["恶魔布袍"]={300,315,330,345}, +["恶魔布靴"]={285,300,315,330}, +["惊艳帝王石"]={300,310,315,320}, +["戈多克食人魔装"]={275,285,290,295}, +["战熊热裤"]={285,305,315,325}, +["战熊背心"]={275,295,305,315}, +["手工亚麻裤"]={70,95,112,130}, +["手工皮外衣"]={1,40,55,70}, +["手工皮带"]={25,55,70,85}, +["手工皮护腕"]={1,40,55,70}, +["手工皮披风"]={1,40,55,70}, +["手工皮短裤"]={15,45,60,75}, +["手工皮靴"]={1,40,55,70}, +["扳手"]={50,70,80,90}, +["抑制头环"]={135,155,165,175}, +["抗毒药剂"]={80,80,115,150}, +["抗毒药水"]={120,145,165,185}, +["抗魔药水"]={210,225,245,265}, +["护卫披风"]={300,315,330,345}, +["报警机器人"]={265,275,280,285}, +["掘地鼠炖肉"]={90,130,150,170}, +["掠夺者兜帽"]={265,265,265,265}, +["掠夺者手套"]={260,260,260,260}, +["掠夺者披肩"]={265,265,265,265}, +["掠夺者裤子"]={265,265,265,265}, +["掠夺者长袍"]={270,270,270,270}, +["掠夺者鞋子"]={260,260,260,260}, +["支配之权杖"]={300,330,340,350}, +["支配之龙印"]={300,320,330,340}, +["放血剃刀"]={250,0,0,0}, +["敏捷药剂"]={185,205,225,245}, +["斑点黄尾鱼"]={225,265,285,305}, +["无底包"]={300,315,330,345}, +["无暇月亮石"]={175,180,182,185}, +["旭日皇冠"]={250,250,270,290}, +["明亮灰烬石"]={200,205,207,210}, +["明亮瑟银微光"]={255,270,277,285}, +["明亮的铜戒指"]={15,35,45,55}, +["昏睡药水"]={230,245,265,285}, +["星型宝石护腕"]={270,0,0,0}, +["星形护符"]={220,220,240,260}, +["星形瑟银指环"]={260,260,275,290}, +["星形红玉戒指"]={300,0,0,0}, +["星界护符"]={175,0,0,0}, +["星辰腰带"]={200,220,235,250}, +["普通瞄准镜"]={110,135,147,160}, +["智慧药剂"]={90,120,140,160}, +["暗影之力药剂"]={250,265,285,305}, +["暗影之油"]={165,190,210,230}, +["暗影头巾"]={170,190,205,220}, +["暗影宝珠"]={100,120,130,140}, +["暗影护目镜"]={120,145,157,170}, +["暗影皮靴"]={205,225,230,235}, +["暗影石戒指"]={105,125,135,145}, +["暗影防护药水"]={135,160,180,200}, +["暗纹之靴"]={240,255,270,285}, +["暗纹手套"]={225,240,255,270}, +["暗纹护肩"]={235,250,265,280}, +["暗纹短裤"]={210,225,240,255}, +["暗纹长袍"]={215,230,245,260}, +["暗纹面罩"]={245,260,275,290}, +["暗色护腕"]={185,205,215,225}, +["暗色皮带"]={195,215,225,235}, +["暗色皮护腿"]={165,185,195,205}, +["暗色皮甲"]={175,195,205,215}, +["暗色长靴"]={200,220,230,240}, +["暗草护腕"]={80,160,170,180}, +["暴怒药水"]={175,195,215,235}, +["曙光护腕"]={115,135,142,150}, +["月光咒符"]={275,0,0,0}, +["月光外衣"]={90,115,130,145}, +["月光法杖"]={125,150,160,170}, +["月布"]={250,290,305,320}, +["月布包"]={300,315,330,345}, +["月布外衣"]={300,315,330,345}, +["月布头饰"]={300,315,330,345}, +["月布手套"]={300,315,330,345}, +["月布护肩"]={300,315,330,345}, +["月布护腿"]={290,305,320,335}, +["月布长袍"]={300,315,330,345}, +["月布长靴"]={290,295,310,325}, +["月牙斧"]={200,225,237,250}, +["月钢宽剑"]={180,205,217,230}, +["有限无敌药水"]={250,275,295,315}, +["木喉之力"]={290,310,320,330}, +["木喉之怒"]={290,310,320,330}, +["木喉之智"]={290,305,320,335}, +["木喉作战手套"]={300,320,330,340}, +["木喉肩铠"]={300,325,337,350}, +["木喉衬肩"]={300,315,330,345}, +["未淬火的符文剑"]={300,320,330,340}, +["机械修理包"]={200,200,220,240}, +["机械幼龙"]={200,220,230,240}, +["机械松鼠"]={75,105,120,135}, +["杂味炖肉"]={75,115,135,155}, +["杂烩蚌肉"]={90,130,150,170}, +["林栖者外衣"]={300,315,330,345}, +["林栖者头冠"]={300,315,330,345}, +["林栖者护肩"]={300,315,330,345}, +["柔光戒指"]={65,85,92,100}, +["柔光照明法杖"]={210,235,245,255}, +["柔韧奥术石"]={300,320,330,340}, +["梦境丝线"]={300,300,300,300}, +["梦境丝线手套"]={300,300,300,300}, +["梦境丝线护腕"]={300,300,300,300}, +["梦境丝线披肩"]={300,300,300,300}, +["梦境丝线裙裤"]={300,300,300,300}, +["梦境使者"]={300,320,330,340}, +["梦境火酒药剂"]={300,0,0,0}, +["梦境皮革"]={300,320,330,340}, +["梦境皮革护腕"]={300,320,330,340}, +["梦境皮革护腿"]={300,320,330,340}, +["梦境皮革披肩"]={300,320,330,340}, +["梦境皮革腰带"]={300,320,330,340}, +["梦境精华药剂"]={300,315,322,330}, +["梦境药剂"]={240,255,275,295}, +["梦境钢铁护肩"]={300,0,0,0}, +["梦境钢铁护腕"]={300,325,337,350}, +["梦境钢铁护腿"]={300,325,337,350}, +["梦境钢铁长靴"]={300,325,337,350}, +["梦幻龙鳞胸甲"]={300,320,330,340}, +["梦纹外衣"]={225,240,255,270}, +["梦纹头饰"]={250,265,280,295}, +["梦纹手套"]={225,240,255,270}, +["梦钢带扣"]={300,325,337,350}, +["棕色亚麻外衣"]={10,45,57,70}, +["棕色亚麻短裤"]={30,55,72,90}, +["棕色亚麻衬衣"]={1,35,47,60}, +["棕色亚麻长袍"]={30,55,72,90}, +["棱光护符"]={265,265,285,305}, +["棱光鳞甲头盔"]={300,300,305,310}, +["橙色军用衬衣"]={220,225,230,235}, +["橙色魔纹衬衣"]={215,220,225,230}, +["次级巫师之油"]={200,0,0,0}, +["次级巫师袍"]={135,155,170,185}, +["次级敏捷药剂"]={140,165,185,205}, +["次级治疗药水"]={55,85,105,125}, +["次级法力之油"]={250,0,0,0}, +["次级法力药水"]={120,145,165,185}, +["次级石盾药水"]={215,230,250,270}, +["次级秘法魔杖"]={155,0,0,0}, +["次级隐形药水"]={165,185,205,225}, +["次级魔法杖"]={10,0,0,0}, +["歼灭者"]={300,320,330,340}, +["毁灭红玉指环"]={300,330,350,370}, +["毁灭者的黑暗之握"]={300,320,330,340}, +["比兹尼克247x128精确瞄准镜"]={300,320,330,340}, +["毛布卷"]={75,90,97,105}, +["毛皮护甲片"]={250,255,265,275}, +["毛纺包"]={80,105,122,140}, +["毛纺斗篷"]={75,100,117,135}, +["毛纺靴"]={95,120,137,155}, +["水下呼吸药剂"]={90,120,140,160}, +["水下诱鱼器"]={150,150,160,170}, +["水手的陨落"]={190,210,220,230}, +["水晶护腕"]={280,285,297,310}, +["水晶火焰护腕"]={255,275,285,295}, +["水晶耳环"]={185,205,215,225}, +["水煮蚌肉"]={50,90,110,130}, +["水煮阳鳞鲑鱼"]={250,290,310,330}, +["水银环"]={265,285,292,300}, +["永恒梦境碎片"]={300,0,0,0}, +["沙漠肉丸子"]={285,325,345,365}, +["沙行者护手"]={300,320,330,340}, +["沙行者护腕"]={300,320,330,340}, +["沙行者胸甲"]={300,320,330,340}, +["油炸红腮鱼"]={225,265,285,305}, +["治疗药水"]={110,135,155,175}, +["法力药水"]={160,180,200,220}, +["法拉基祭祀图腾"]={140,160,170,180}, +["法术掌握手套"]={300,315,330,345}, +["法术能量护目镜超级改良版"]={270,290,300,310}, +["法术能量护目镜超级版"]={225,245,255,265}, +["泛光月亮石胸针"]={185,200,207,215}, +["泰坦之锤"]={300,320,330,340}, +["泰坦合剂"]={300,315,322,330}, +["泰坦护腿"]={300,320,330,340}, +["洛丹伦胸甲"]={100,275,295,310}, +["洛克湖狂鱼"]={50,90,110,130}, +["洛恩塔姆薯块"]={275,315,335,355}, +["洞察束带"]={300,320,330,340}, +["活力行动药水"]={285,300,320,340}, +["活动假人"]={85,115,130,145}, +["浓烟山脉之心"]={265,0,0,0}, +["海员香辣炖菜"]={35,75,95,115}, +["海洋之心"]={200,220,230,240}, +["海洋之怒"]={115,120,130,140}, +["海洋之根"]={200,215,225,235}, +["海龟汤"]={175,215,235,255}, +["淡黄色衬衣"]={135,145,150,155}, +["深沉白玉"]={270,270,272,275}, +["深海护手"]={230,250,260,270}, +["深海的凝视"]={190,190,210,230}, +["深海项链"]={85,105,115,125}, +["深渊猎手头盔"]={300,0,0,0}, +["深红丝质外衣"]={185,205,215,225}, +["深红丝质手套"]={210,225,240,255}, +["深红丝质护肩"]={190,210,225,240}, +["深红丝质披风"]={180,200,215,230}, +["深红丝质腰带"]={175,195,210,225}, +["深红丝质长袍"]={205,220,235,250}, +["深红丝质马裤"]={195,215,225,235}, +["深红护卫法杖"]={290,0,0,0}, +["清醒药水"]={300,315,322,330}, +["渴魔手套"]={75,150,167,185}, +["溶解毒药"]={1,285,310,335}, +["溶解毒药 II"]={1,325,350,375}, +["滋补药剂"]={210,225,245,265}, +["滑皮鲭鱼"]={1,45,65,85}, +["漂亮的红衬衣"]={110,135,152,170}, +["漂亮的绿衬衣"]={120,145,162,180}, +["漂亮的蓝衬衣"]={120,145,162,180}, +["漆黑曜石"]={285,290,292,295}, +["潜水头盔"]={230,250,260,270}, +["激发潜能之戒"]={290,320,330,340}, +["灌注精华手套"]={300,300,305,310}, +["火力药剂"]={140,165,185,205}, +["火山战锤"]={290,315,327,340}, +["火山护肩"]={300,320,330,340}, +["火山护腿"]={270,290,300,310}, +["火山胸甲"]={285,305,315,325}, +["火怒护腿"]={300,300,300,300}, +["火核狙击步枪"]={300,320,330,340}, +["火焰之油"]={130,150,160,170}, +["火焰偏斜器"]={125,125,150,175}, +["火焰勋章"]={110,130,140,150}, +["火焰头盔"]={250,270,280,290}, +["火焰护目镜"]={205,225,235,245}, +["火焰披风"]={275,290,305,320}, +["火焰防护药水"]={165,210,230,250}, +["火链胸甲"]={300,325,337,350}, +["灰布外衣"]={260,275,290,305}, +["灰布手套"]={270,285,300,315}, +["灰布披风"]={275,290,305,320}, +["灰布短裤"]={280,295,310,325}, +["灰布长袍"]={225,240,255,270}, +["灰布长靴"]={245,260,275,290}, +["灰烬堕落戒指"]={260,280,290,300}, +["灰烬宝石护腕"]={300,320,330,340}, +["灰烬石神像"]={220,240,250,260}, +["灰烬石镶嵌指环"]={225,240,247,255}, +["灰色毛纺衬衣"]={100,110,120,130}, +["灰色毛纺长袍"]={105,130,147,165}, +["灵魂袋"]={260,275,290,305}, +["灼热金剑"]={190,215,227,240}, +["炖陆行鸟"]={50,90,110,130}, +["炖龙虾"]={275,315,335,355}, +["炫光头盔"]={300,320,330,340}, +["炫光护腿"]={300,320,330,340}, +["炫光肩铠"]={300,320,330,340}, +["炫光胸甲"]={300,320,330,340}, +["炸弹宠物"]={205,205,205,205}, +["点金石"]={225,0,0,0}, +["点铁成金"]={225,240,260,280}, +["炼金石"]={300,315,322,330}, +["炽热板甲护手"]={290,310,320,330}, +["炽热红宝石"]={200,205,207,210}, +["炽热链甲护肩"]={300,320,330,340}, +["炽热链甲束带"]={295,315,325,335}, +["烈性火药"]={125,125,135,145}, +["烈性炸药"]={125,125,135,145}, +["烈日黄玉戒指"]={185,200,210,220}, +["烈焰核心靴子"]={300,300,300,300}, +["烟熏胸针"]={200,220,230,240}, +["烟熏鲈鱼"]={240,280,300,320}, +["烟花发射器"]={225,245,255,265}, +["烟花束发射器"]={275,295,305,315}, +["烤狮排"]={125,175,195,215}, +["烤科多肉"]={35,75,95,115}, +["烤迅猛龙肉"]={175,215,235,255}, +["烤野猪肉"]={1,45,65,85}, +["烤鱿鱼"]={240,280,300,320}, +["烤鲑鱼"]={275,315,335,355}, +["烤鼠尾鱼"]={80,120,140,160}, +["烧烤巨蚌"]={175,215,235,255}, +["烧烤狼肉"]={1,45,65,85}, +["烧烤秃鹰翅膀"]={175,215,235,255}, +["热狼排"]={175,215,235,255}, +["煮熟的光滑大鱼"]={225,265,285,305}, +["煮蟹爪"]={85,125,145,165}, +["煽动毒药I"]={300,0,0,0}, +["熏熊肉"]={40,80,100,120}, +["熔岩护腿"]={300,300,300,300}, +["熔岩犬皮靴"]={295,315,325,335}, +["熔岩腰带"]={300,320,330,340}, +["熔火升华之冠"]={300,320,330,340}, +["熔火恶魔布包"]={300,315,330,345}, +["熔火护甲片"]={300,320,330,340}, +["熔火犬皮腰带"]={300,320,330,340}, +["熔火猎犬手套"]={300,300,300,300}, +["熔火腰带"]={300,320,330,340}, +["熔炼梦境钢锭"]={300,350,362,375}, +["熔炼源质"]={300,350,362,375}, +["熔炼瑟银"]={250,250,250,250}, +["熔炼真银"]={230,230,230,230}, +["熔炼秘银"]={175,175,175,175}, +["熔炼金锭"]={155,170,177,185}, +["熔炼钢锭"]={165,165,165,165}, +["熔炼铁锭"]={125,125,130,140}, +["熔炼铜锭"]={1,25,45,70}, +["熔炼银锭"]={75,100,112,125}, +["熔炼锡锭"]={65,65,70,75}, +["熔炼青铜"]={65,65,90,115}, +["熔炼黑铁"]={230,230,230,230}, +["熔铸头盔"]={300,320,330,340}, +["熟化中毛皮"]={100,115,122,130}, +["熟化厚毛皮"]={200,200,200,200}, +["熟化毛皮"]={250,250,255,260}, +["熟化轻毛皮"]={35,55,65,75}, +["熟化重毛皮"]={150,160,165,170}, +["燃星宝石"]={225,225,227,230}, +["爆炸护盾"]={75,100,120,140}, +["牙爪圣物"]={120,140,150,160}, +["特效巨魔之血药水"]={290,305,325,345}, +["特效抗毒药剂"]={300,300,330,360}, +["特效治疗药水"]={275,290,310,330}, +["特效法力药水"]={295,310,330,350}, +["特效活力药水"]={300,310,320,330}, +["牺牲长袍"]={300,300,300,300}, +["狂鱼肉片"]={50,90,110,130}, +["狙击瞄准镜"]={240,260,270,280}, +["狮心头盔"]={300,320,330,340}, +["狮王之力药剂"]={1,55,75,95}, +["狼头之盔"]={225,245,255,265}, +["猛毒尖冠"]={145,165,175,185}, +["猝火艾泽拉斯钻石"]={275,280,282,285}, +["猪肉干"]={80,120,140,160}, +["猪肝馅饼"]={50,90,110,130}, +["猫眼药剂"]={200,220,240,260}, +["猫眼超级护目镜"]={220,240,250,260}, +["猫鼬药剂"]={280,295,315,335}, +["猫鼬长靴"]={300,320,330,340}, +["献祭之油"]={205,220,240,260}, +["玉蛇刀"]={175,200,212,225}, +["王之决断"]={85,105,115,125}, +["玛瑙头冠"]={125,145,155,165}, +["玫瑰色护目镜"]={230,250,260,270}, +["珍珠匕首"]={110,140,155,170}, +["珍珠披风"]={90,115,132,150}, +["琥珀宝珠"]={95,110,120,130}, +["琥珀指环"]={60,80,87,95}, +["琥珀石吊坠"]={80,105,115,125}, +["瑟银头盔"]={280,300,310,320}, +["瑟银巨剑"]={260,285,297,310}, +["瑟银带扣"]={240,265,277,290}, +["瑟银弹"]={285,305,315,325}, +["瑟银手榴弹"]={260,280,290,300}, +["瑟银护甲"]={250,270,280,290}, +["瑟银护腕"]={255,275,285,295}, +["瑟银护腿"]={300,320,330,340}, +["瑟银火枪"]={260,280,290,300}, +["瑟银盾刺"]={275,295,305,315}, +["瑟银管"]={275,295,305,315}, +["瑟银腰带"]={250,270,280,290}, +["瑟银长靴"]={280,300,310,320}, +["瑟银零件"]={260,280,290,300}, +["瑟银马刺"]={275,300,310,320}, +["璀璨玛瑙"]={135,135,137,140}, +["生命护肩"]={270,290,300,310}, +["生命护腿"]={285,305,315,325}, +["生命胸甲"]={300,320,330,340}, +["电动惩戒器"]={250,270,280,290}, +["电压中和自然反射器"]={290,310,320,330}, +["瘦狼排"]={125,165,185,205}, +["瘦鹿肉"]={110,150,170,190}, +["白玉手镯"]={250,280,290,300}, +["白玉指环"]={290,320,330,340}, +["白色亚麻衬衣"]={1,35,47,60}, +["白色亚麻长袍"]={30,55,72,90}, +["白色冒险者衬衣"]={160,170,175,180}, +["白色婚纱"]={250,255,260,265}, +["白色强盗面罩"]={215,220,225,230}, +["白色毛绒裙"]={110,135,152,170}, +["白色皮夹克"]={60,90,105,120}, +["白银万能钥匙"]={100,100,110,120}, +["白魂头盔"]={300,320,330,340}, +["皇家宝石法杖"]={215,235,245,255}, +["皇家曙光权杖"]={240,260,275,290}, +["皮质小弹药包"]={30,60,75,90}, +["皱褶皮短裤"]={35,65,80,95}, +["盖亚的拥抱"]={300,315,330,345}, +["真银万能钥匙"]={200,200,210,220}, +["真银变压器"]={260,270,275,280}, +["真银圣剑"]={260,285,297,310}, +["真银带扣"]={225,225,230,235}, +["真银护手"]={225,245,255,265}, +["真银棒"]={200,205,207,210}, +["真银胸甲"]={245,265,275,285}, +["石之护符"]={100,125,135,145}, +["石英指环"]={155,175,185,195}, +["石鳞鱼油"]={250,250,255,260}, +["石鳞鳕鱼"]={175,190,210,230}, +["破冰护手"]={300,320,330,340}, +["破冰护腕"]={300,320,330,340}, +["破冰胸甲"]={300,320,330,340}, +["破碎守护者的护符"]={300,320,325,330}, +["硬化蝎壳头盔"]={250,270,280,290}, +["硬化蝎壳战靴"]={235,255,265,275}, +["硬化蝎壳手套"]={225,245,255,265}, +["硬化蝎壳护肩"]={240,260,270,280}, +["硬化蝎壳护腕"]={220,240,250,260}, +["硬化蝎壳护腿"]={245,265,275,285}, +["硬化蝎壳胸甲"]={220,240,250,260}, +["硬化铜手镯"]={50,70,75,80}, +["硬化青铜法杖"]={100,120,130,140}, +["硬甲皮"]={250,250,250,250}, +["硬铁短剑"]={160,185,197,210}, +["碎石穴居怪伪装"]={1,0,0,0}, +["碎裂黑曜石盾牌"]={300,320,330,340}, +["碎铁金锤"]={170,195,207,220}, +["碧蓝丝质外衣"]={150,170,185,200}, +["碧蓝丝质头巾"]={145,155,160,165}, +["碧蓝丝质手套"]={145,165,180,195}, +["碧蓝丝质披风"]={175,195,210,225}, +["碧蓝丝质短裤"]={140,160,175,190}, +["碧蓝丝质腰带"]={175,195,210,225}, +["碧蓝戒指"]={60,80,87,95}, +["碧蓝护肩"]={190,210,225,240}, +["礼服夹克"]={250,265,280,295}, +["礼服短裤"]={245,250,255,260}, +["礼服衬衣"]={240,245,250,255}, +["神圣防护药水"]={100,130,150,170}, +["神秘杂烩"]={175,215,235,255}, +["禁锢徽记"]={125,145,155,165}, +["科多兽皮包"]={40,70,85,100}, +["科多肉杂烩"]={200,240,260,280}, +["秘银外壳"]={215,215,235,255}, +["秘银带扣"]={185,210,222,235}, +["秘银机械幼龙"]={250,270,280,290}, +["秘银杆"]={200,225,237,250}, +["秘银杆设计图"]={205,205,205,205}, +["秘银火枪"]={205,225,235,245}, +["秘银盾刺"]={215,235,245,255}, +["秘银短裤"]={210,230,240,250}, +["秘银破片炸弹"]={215,215,235,255}, +["秘银符文战锤"]={245,270,282,295}, +["秘银管"]={195,195,215,235}, +["秘银细剑"]={240,265,277,290}, +["秘银罩帽"]={230,250,260,270}, +["秘银螺旋弹"]={245,245,265,285}, +["秘银重斧"]={210,235,247,260}, +["秘银重靴"]={235,255,265,275}, +["秘银马刺"]={235,255,265,275}, +["秘银魔剑"]={225,250,262,275}, +["秘银鳞片手套"]={220,240,250,260}, +["秘银鳞片护肩"]={235,255,265,275}, +["秘银鳞片护腕"]={215,235,245,255}, +["秘银鳞片短裤"]={210,230,240,250}, +["秘银黑石项链"]={245,265,275,285}, +["移形披风"]={300,320,330,340}, +["究极充能奥术反射器"]={290,310,320,330}, +["空间撕裂器 - 永望镇"]={260,0,0,0}, +["窃贼手套"]={140,165,177,190}, +["符文之刃"]={285,310,322,335}, +["符文冥河护腿"]={300,315,330,345}, +["符文冥河腰带"]={300,315,330,345}, +["符文冥河长靴"]={300,315,330,345}, +["符文奥金棒"]={290,0,0,0}, +["符文布卷"]={250,255,257,260}, +["符文布外套"]={260,275,290,305}, +["符文布头带"]={295,310,325,340}, +["符文布手套"]={275,290,305,320}, +["符文布护肩"]={300,315,330,345}, +["符文布披风"]={265,280,295,310}, +["符文布短裤"]={285,300,315,330}, +["符文布绷带"]={260,260,290,320}, +["符文布背包"]={260,275,290,305}, +["符文布腰带"]={255,270,285,300}, +["符文布袍"]={260,275,290,305}, +["符文布靴"]={280,295,310,325}, +["符文护符"]={230,230,252,275}, +["符文板甲"]={300,320,330,340}, +["符文板甲头盔"]={300,320,330,340}, +["符文板甲战靴"]={300,320,330,340}, +["符文板甲护肩"]={300,320,330,340}, +["符文板甲护腿"]={300,320,330,340}, +["符文皮甲"]={300,320,330,340}, +["符文皮甲头环"]={290,310,320,330}, +["符文皮甲护手"]={270,290,300,310}, +["符文皮甲护肩"]={300,320,330,340}, +["符文皮甲护腕"]={275,295,305,315}, +["符文皮甲短裤"]={300,320,330,340}, +["符文皮甲腰带"]={280,300,310,320}, +["符文真银戒指"]={220,240,250,260}, +["符文真银棒"]={200,0,0,0}, +["符文蚀刻之握"]={300,320,330,340}, +["符文蚀刻护肩"]={300,320,330,340}, +["符文蚀刻王冠"]={300,320,330,340}, +["符文蚀刻胫甲"]={300,320,330,340}, +["符文蚀刻胸甲"]={300,320,330,340}, +["符文蚀刻腿甲"]={300,320,330,340}, +["符文金棒"]={150,0,0,0}, +["符文铜棒"]={1,0,0,0}, +["符文银棒"]={100,0,0,0}, +["笨重的铜戒指"]={25,25,45,65}, +["筛盐器"]={250,270,280,290}, +["简易亚麻短裤"]={1,35,47,60}, +["简易投掷炸弹"]={100,115,122,130}, +["简易投掷炸弹II"]={200,200,210,220}, +["简易的亚麻靴"]={20,50,67,85}, +["简易的裙子"]={40,65,82,100}, +["简易的褶裙"]={75,100,117,135}, +["简易的黑裙子"]={235,240,245,250}, +["粉碎者"]={235,260,272,285}, +["粉色魔纹衬衣"]={235,240,245,250}, +["粗制平衡石"]={65,65,72,80}, +["粗制火药粉"]={75,85,90,95}, +["粗制炸药"]={75,90,97,105}, +["粗制瞄准镜"]={60,90,105,120}, +["粗制砂轮"]={75,75,87,100}, +["粗制磨刀石"]={65,65,72,80}, +["粗糙的宝石串"]={125,140,142,145}, +["粗糙的金戒指"]={150,165,170,175}, +["粗糙的铁戒指"]={150,165,170,175}, +["粗糙砂纸"]={70,90,95,100}, +["粗糙青铜戒指"]={90,100,107,115}, +["粗野的聚焦手杖"]={300,330,340,350}, +["精制奥妮克希亚鳞片"]={300,0,0,0}, +["精制实心弹丸"]={125,125,135,145}, +["精制望远镜"]={135,160,172,185}, +["精制秘银头盔"]={210,265,275,285}, +["精制秘银战靴"]={210,265,275,285}, +["精制秘银手套"]={220,240,250,260}, +["精制秘银护肩"]={225,245,255,265}, +["精制秘银短裤"]={220,240,250,260}, +["精制秘银胸甲"]={210,260,270,280}, +["精制轻弹丸"]={1,30,45,60}, +["精制重弹丸"]={75,85,90,95}, +["精制钻石手镯"]={300,325,332,340}, +["精密奥金转换器"]={285,305,315,325}, +["精密珠宝工具包"]={175,185,190,195}, +["精密陀螺仪护目镜"]={300,320,330,340}, +["精工风暴战锤"]={300,320,330,340}, +["精炼智慧合剂"]={300,315,322,330}, +["精确瞄准镜"]={180,200,210,220}, +["精致手工火枪"]={120,145,157,170}, +["精致的宝石串"]={200,200,205,210}, +["精致的矮人项链"]={185,185,200,215}, +["精致的金手镯"]={200,220,230,240}, +["精致秘银头冠"]={210,230,240,250}, +["精致秘银护符"]={180,200,210,220}, +["精致钻石宝冠"]={300,320,330,340}, +["精钢战靴"]={185,210,222,235}, +["紫色丝质衬衣"]={185,195,200,205}, +["紫色魔纹衬衣"]={230,235,240,245}, +["红色亚麻包"]={70,95,112,130}, +["红色亚麻外衣"]={55,80,97,115}, +["红色亚麻衬衣"]={40,65,82,100}, +["红色亚麻长袍"]={40,65,82,100}, +["红色冒险者衬衣"]={175,185,190,195}, +["红色毛纺包"]={115,140,157,175}, +["红色毛纺靴"]={95,120,137,155}, +["红色烟花束"]={225,225,237,250}, +["红色焰火"]={150,150,162,175}, +["红色节庆裤装"]={250,265,280,295}, +["红色节庆长裙"]={250,0,0,0}, +["红色雏龙手套"]={120,145,157,170}, +["红色魔纹包"]={235,250,265,280}, +["红色魔纹外衣"]={215,230,245,260}, +["红色魔纹头带"]={240,255,270,285}, +["红色魔纹手套"]={225,240,255,270}, +["红色魔纹护肩"]={235,250,265,280}, +["红色魔纹短裤"]={215,230,245,260}, +["红龙鳞片护肩"]={300,300,300,300}, +["红龙鳞片护腿"]={295,300,300,300}, +["红龙鳞片胸甲"]={300,320,330,340}, +["红龙鳞片长靴"]={295,300,300,300}, +["纯金戒指"]={295,0,0,0}, +["纯银戒指"]={135,155,165,175}, +["练习锁"]={100,115,122,130}, +["织法者之杖"]={265,285,292,300}, +["织法者坠饰"]={265,275,285,295}, +["绒线绷带"]={80,80,115,150}, +["结实的铁锤"]={155,180,192,205}, +["绚丽宝钻"]={270,275,277,280}, +["绽放翡翠之杖"]={165,185,195,205}, +["绿色丝甲"]={165,185,200,215}, +["绿色丝质包"]={175,195,210,225}, +["绿色丝质护肩"]={180,200,215,230}, +["绿色亚麻护腕"]={60,85,102,120}, +["绿色亚麻衬衣"]={70,95,112,130}, +["绿色幼龙护甲"]={175,195,205,215}, +["绿色幼龙护腕"]={190,210,220,230}, +["绿色护目镜"]={150,175,187,200}, +["绿色毛纺包"]={95,120,137,155}, +["绿色毛纺外衣"]={85,110,127,145}, +["绿色毛纺长袍"]={90,0,0,0}, +["绿色烟花束"]={225,225,237,250}, +["绿色焰火"]={150,150,162,175}, +["绿色皮带"]={160,180,190,200}, +["绿色皮护腕"]={180,200,210,220}, +["绿色皮甲"]={155,175,185,195}, +["绿色节日衬衣"]={190,200,205,210}, +["绿色透镜"]={245,265,275,285}, +["绿色龙鳞护手"]={280,300,310,320}, +["绿铁头盔"]={170,195,207,220}, +["绿铁战靴"]={145,175,190,205}, +["绿铁护手"]={150,180,195,210}, +["绿铁护肩"]={160,185,197,210}, +["绿铁护腕"]={165,190,202,215}, +["绿铁护腿"]={155,180,192,205}, +["绿铁锁甲"]={180,205,217,230}, +["绿龙鳞片护腿"]={270,290,300,310}, +["绿龙鳞片胸甲"]={260,280,290,300}, +["缀玉戒指"]={70,90,95,100}, +["缚风者手套"]={300,315,330,345}, +["缥缈的霜花皇冠"]={280,285,297,310}, +["美味小鱼"]={1,45,65,85}, +["美味煎蛋卷"]={130,170,190,210}, +["美味风蛇"]={85,125,145,165}, +["美味鼠尾鱼"]={175,215,235,255}, +["羽饰胸甲"]={250,270,280,290}, +["翡翠和谐指环"]={170,190,197,205}, +["翡翠帝王石"]={300,320,330,340}, +["翡翠梦境者护胸"]={300,300,300,300}, +["翡翠猫鼬药剂"]={300,0,0,0}, +["翼爪戒指"]={145,165,175,185}, +["联合收割机组件"]={175,175,195,215}, +["脆炸蜥蜴尾"]={100,140,160,180}, +["腐肉大餐"]={175,215,235,255}, +["腐蚀术"]={290,315,327,340}, +["腐蚀毒药"]={1,280,305,330}, +["腐蚀毒药 II"]={1,300,325,350}, +["自动净化装置"]={140,165,177,190}, +["自然防护药水"]={190,210,230,250}, +["自爆绵羊"]={150,175,187,200}, +["自由行动药剂"]={150,175,195,215}, +["致伤毒药"]={1,185,210,235}, +["致伤毒药 II"]={180,0,0,0}, +["致伤毒药 III"]={220,0,0,0}, +["致伤毒药 IV"]={260,0,0,0}, +["致命毒药"]={130,175,200,225}, +["致命毒药 II"]={170,215,240,265}, +["致命毒药 III"]={210,255,280,305}, +["致命毒药 IV"]={250,295,320,345}, +["致命毒药 V"]={1,300,325,350}, +["致命的短枪"]={105,130,142,155}, +["致命的青铜短剑"]={125,155,170,185}, +["致命瞄准镜"]={210,230,240,250}, +["致密平衡石"]={250,255,257,260}, +["致密炸药"]={250,250,260,270}, +["致密炸药粉"]={250,250,255,260}, +["致密的宝石串"]={235,240,240,240}, +["致密砂纸"]={250,260,265,270}, +["致密砂轮"]={250,255,257,260}, +["致密磨刀石"]={250,255,257,260}, +["致残毒药"]={1,125,150,175}, +["致残毒药 II"]={230,275,300,325}, +["致盲粉"]={1,170,195,220}, +["舒适的皮帽"]={200,220,230,240}, +["艾泽拉斯红玉"]={275,280,282,285}, +["苏生黄玉"]={300,320,330,340}, +["苦工伪装"]={1,0,0,0}, +["荆木头盔"]={300,320,330,340}, +["荆木腰带"]={300,320,330,340}, +["荆木长靴"]={300,320,330,340}, +["草药烘蛋"]={1,45,65,85}, +["荒芜"]={250,275,287,300}, +["菊花茶"]={60,100,120,140}, +["萨弗隆战锤"]={300,325,337,350}, +["蓝光斧"]={220,245,257,270}, +["蓝玉指环"]={290,320,330,340}, +["蓝色亚麻外衣"]={55,80,97,115}, +["蓝色亚麻衬衣"]={40,65,82,100}, +["蓝色亚麻长袍"]={70,95,112,130}, +["蓝色烟花束"]={225,225,237,250}, +["蓝色焰火"]={150,150,162,175}, +["蓝色罩衫"]={100,125,142,160}, +["蓝色龙鳞长靴"]={290,310,320,330}, +["蓝龙鳞片护肩"]={295,315,325,335}, +["蓝龙鳞片护腿"]={300,320,330,340}, +["蓝龙鳞片胸甲"]={285,305,315,325}, +["虎纹戒指"]={50,70,77,85}, +["虚灵外套"]={300,300,300,300}, +["虚灵头饰"]={300,300,300,300}, +["虚灵护肩"]={300,300,300,300}, +["虚灵护腿"]={300,300,300,300}, +["虚空之心咒符"]={280,280,300,320}, +["虚空法袍"]={300,315,330,345}, +["蛇盘之杖"]={100,120,130,140}, +["蛇鳞手套"]={105,130,142,155}, +["蛇鳞披风"]={90,120,135,150}, +["蛇鳞腰带"]={115,140,152,165}, +["蛋奶酒"]={35,75,95,115}, +["蛛丝之靴"]={125,150,167,185}, +["蛛丝便鞋"]={140,160,175,190}, +["蛮力药剂"]={275,290,310,330}, +["蛮皮外衣"]={225,245,255,265}, +["蛮皮头盔"]={225,245,255,265}, +["蛮皮战靴"]={245,265,275,285}, +["蛮皮护肩"]={220,240,250,260}, +["蛮皮护腿"]={250,270,280,290}, +["蛮皮披风"]={250,270,280,290}, +["蜘蛛肉肠"]={200,240,260,280}, +["蜘蛛腰带"]={180,200,215,230}, +["蜘蛛蛋糕"]={110,150,170,190}, +["蜜汁烤猪排"]={175,215,235,255}, +["蝎肉大餐"]={20,60,80,100}, +["蟹肉蛋糕"]={75,115,135,155}, +["血火头饰"]={200,220,230,240}, +["血爪"]={300,320,330,340}, +["血石战刃"]={225,260,267,275}, +["血肠"]={60,100,120,140}, +["血腥带扣"]={300,325,337,350}, +["血藤外套"]={300,315,330,345}, +["血藤护目镜"]={300,320,330,340}, +["血藤护腿"]={300,315,330,345}, +["血藤透镜"]={300,320,330,340}, +["血藤长靴"]={300,315,330,345}, +["血虎护肩"]={300,320,330,340}, +["血虎胸甲"]={300,320,330,340}, +["血魂护手"]={300,320,330,340}, +["血魂护肩"]={300,320,330,340}, +["血魂胸甲"]={300,320,330,340}, +["要塞护手"]={300,320,330,340}, +["观星者法袍"]={300,0,0,0}, +["解谜手套"]={300,300,300,300}, +["设计图:金带扣"]={175,175,180,185}, +["语音增强模组"]={290,310,320,330}, +["说服者"]={300,320,330,340}, +["贵族编织斗篷"]={300,0,0,0}, +["赤脊山炖肉"]={100,135,155,175}, +["超凡护腿"]={300,320,330,340}, +["超凡罩帽"]={300,320,330,340}, +["超凡肩甲"]={300,320,330,340}, +["超凡胸甲"]={300,320,330,340}, +["超强巨魔之血药水"]={180,200,220,240}, +["超强防御药剂"]={265,280,300,320}, +["超级煎蛋卷"]={225,265,285,305}, +["超级能量合剂"]={300,315,322,330}, +["超能电池组"]={250,250,260,270}, +["转化秘银"]={225,240,260,280}, +["转化:元素之土"]={300,315,322,330}, +["转化:元素之水"]={300,315,322,330}, +["转化:元素火焰"]={300,301,305,310}, +["转化:土转生命"]={275,275,282,290}, +["转化:奥金"]={275,275,282,290}, +["转化:死灵化水"]={275,275,282,290}, +["转化:水转死灵"]={275,275,282,290}, +["转化:点气成火"]={275,275,282,290}, +["转化:点水成气"]={275,275,282,290}, +["转化:点火成土"]={275,275,282,290}, +["转化:生命归土"]={275,275,282,290}, +["转化:转土成水"]={275,275,282,290}, +["软底亚麻靴"]={80,105,122,140}, +["轻型护甲片"]={1,30,45,60}, +["轻型黑曜石腰带"]={300,320,330,340}, +["轻巧的皮手套"]={120,145,157,170}, +["轻皮"]={1,20,30,40}, +["轻皮护腕"]={70,100,115,130}, +["轻皮短裤"]={95,125,140,155}, +["轻皮箭袋"]={30,60,75,90}, +["辉光蓝宝石"]={290,0,0,0}, +["辉煌女王的皇冠"]={300,320,330,340}, +["辉煌蓝宝石"]={250,0,0,0}, +["辐光头饰"]={295,315,325,335}, +["辐光手套"]={285,305,315,325}, +["辐光护腿"]={300,320,330,340}, +["辐光胸甲"]={270,290,300,310}, +["辐光腰带"]={260,280,290,300}, +["辐光长靴"]={290,310,320,330}, +["辛德拉长老的高贵法杖"]={300,330,350,370}, +["辛迪加伪装"]={1,0,0,0}, +["辣椒蟹肉"]={225,265,285,305}, +["达农佐的泰拉比姆情调"]={300,300,300,300}, +["达农佐的泰拉比姆惊喜"]={300,300,300,300}, +["达农佐的泰拉比姆趣味"]={300,300,300,300}, +["达拉然巫师伪装"]={1,0,0,0}, +["迅捷之靴"]={200,220,230,240}, +["迅捷药水"]={60,90,110,130}, +["迅猛龙皮背心"]={165,185,195,205}, +["迅猛龙皮腰带"]={165,185,195,205}, +["迅行护腕"]={300,320,330,340}, +["迪尔格的超美味奇美拉肉片"]={300,325,345,365}, +["迪菲亚盗贼伪装"]={1,0,0,0}, +["迫击炮:重载"]={205,205,205,205}, +["迷人的黄玉项链"]={200,220,230,240}, +["退化射线"]={160,180,190,200}, +["透明治疗药水"]={50,80,100,120}, +["速效毒药"]={1,125,150,175}, +["速效毒药 II"]={120,165,190,215}, +["速效毒药 III"]={160,205,230,255}, +["速效毒药 IV"]={200,245,270,295}, +["速效毒药 V"]={240,285,340,335}, +["速效毒药 VI"]={280,325,350,375}, +["速游药水"]={100,130,150,170}, +["造弓师手套"]={125,150,162,175}, +["邪恶皮甲"]={300,320,330,340}, +["邪恶皮甲头环"]={280,300,310,320}, +["邪恶皮甲护手"]={260,280,290,300}, +["邪恶皮甲护腕"]={265,285,295,305}, +["邪恶皮甲短裤"]={290,310,320,330}, +["邪恶皮甲腰带"]={300,320,330,340}, +["采药人手套"]={135,160,172,185}, +["重型土灵手套"]={145,170,182,195}, +["重型护甲片"]={150,170,180,190}, +["重型木喉腰带"]={290,310,320,330}, +["重型木喉长靴"]={300,320,330,340}, +["重型秘银头盔"]={245,255,265,275}, +["重型秘银手套"]={205,225,235,245}, +["重型秘银护肩"]={205,225,235,245}, +["重型秘银胸甲"]={230,250,260,270}, +["重型箭袋"]={150,170,180,190}, +["重型蝎壳外衣"]={265,285,295,305}, +["重型蝎壳头盔"]={295,315,325,335}, +["重型蝎壳护手"]={275,295,305,315}, +["重型蝎壳护肩"]={300,320,330,340}, +["重型蝎壳护腕"]={255,275,285,295}, +["重型蝎壳护腿"]={285,305,315,325}, +["重型蝎壳腰带"]={280,300,310,320}, +["重型黑曜石腰带"]={300,320,330,340}, +["重平衡石"]={125,125,132,140}, +["重皮"]={150,150,155,160}, +["重皮弹药包"]={150,170,180,190}, +["重皮球"]={150,150,155,160}, +["重砂轮"]={125,125,137,150}, +["重磅铁制炸弹"]={190,190,210,230}, +["重磅青铜炸弹"]={140,140,165,190}, +["重磨刀石"]={125,125,132,140}, +["野人亚麻外衣"]={70,95,112,130}, +["野人手套"]={150,170,180,190}, +["野人护肩"]={175,195,205,215}, +["野人护腕"]={155,175,185,195}, +["野人护腿"]={170,190,200,210}, +["野人背心"]={190,210,220,230}, +["野人腰带"]={200,220,230,240}, +["野人铁手套"]={185,210,222,235}, +["野人铁护肩"]={160,185,197,210}, +["野人铁盔"]={175,200,212,225}, +["野人铁质胸甲"]={160,185,197,210}, +["野人铁靴"]={180,205,217,230}, +["野刺锁甲"]={270,290,300,310}, +["野性之皮"]={300,320,330,340}, +["野葡萄药水"]={225,240,260,280}, +["野蛮狂怒"]={300,320,330,340}, +["金棒"]={150,155,157,160}, +["金棘茶"]={175,175,190,205}, +["金焰水晶手镯"]={155,155,160,165}, +["金玉戒指"]={210,235,245,255}, +["金色徽记护符"]={170,180,190,200}, +["金色黎明衬肩"]={300,320,330,340}, +["金鳞战靴"]={200,225,237,250}, +["金鳞护手"]={205,225,235,245}, +["金鳞护肩"]={175,200,212,225}, +["金鳞护腕"]={185,210,222,235}, +["金鳞护腿"]={170,195,207,220}, +["金鳞罩盔"]={190,215,227,240}, +["金鳞胸甲"]={195,220,232,245}, +["钢带扣"]={200,195,200,205}, +["钢质头盔"]={215,235,245,255}, +["钢质武器链"]={190,215,227,240}, +["钢质胸甲"]={200,225,237,250}, +["钢铁板甲头盔"]={230,230,230,230}, +["钢铁板甲护手"]={220,220,220,220}, +["钢铁板甲护肩"]={230,230,230,230}, +["钢铁板甲护腿"]={225,225,225,225}, +["钢铁板甲胸甲"]={225,225,225,225}, +["钢铁板甲长靴"]={220,220,220,220}, +["铁扣环"]={150,0,0,0}, +["铁棒"]={160,160,170,180}, +["铁炉堡护手"]={140,0,0,0}, +["铁炉堡胸甲"]={100,140,160,180}, +["铁炉堡链甲"]={70,110,130,150}, +["铁皮手雷"]={175,175,195,215}, +["铁羽护肩"]={270,290,300,310}, +["铁羽胸甲"]={290,310,320,330}, +["铁花戒指"]={190,205,215,225}, +["铁藤手套"]={300,320,330,340}, +["铁藤胸甲"]={300,320,330,340}, +["铁藤腰带"]={300,320,330,340}, +["铁质平衡锤"]={165,190,202,215}, +["铁质盾刺"]={150,180,195,210}, +["铜带扣"]={25,65,85,105}, +["铜手镯"]={1,21,33,45}, +["铜指虎"]={30,60,62,65}, +["铜斧"]={20,60,80,100}, +["铜杖"]={45,60,67,75}, +["铜管"]={50,80,95,110}, +["铜质匕首"]={30,70,90,110}, +["铜质双刃刀"]={30,70,90,110}, +["铜质大锤"]={65,105,125,145}, +["铜质宝石手套"]={60,100,120,140}, +["铜质战斧"]={35,75,95,115}, +["铜质护腕"]={1,20,40,60}, +["铜质短剑"]={25,65,85,105}, +["铜质符文护手"]={40,80,100,120}, +["铜质符文护腕"]={90,115,127,140}, +["铜质符文短裤"]={45,85,105,125}, +["铜质符文胸甲"]={80,120,140,160}, +["铜质符文腰带"]={70,110,130,150}, +["铜质调节器"]={65,95,110,125}, +["铜质重剑"]={95,135,155,175}, +["铜质钉锤"]={15,55,75,95}, +["铜质链甲外衣"]={35,75,95,115}, +["铜质链甲战靴"]={20,60,80,100}, +["铜质链甲短裤"]={1,50,70,90}, +["铜质链甲腰带"]={35,75,95,115}, +["铭文圣典"]={260,280,287,295}, +["铭文板甲护腿"]={300,320,330,340}, +["银头鲑鱼"]={175,215,235,255}, +["银棒"]={100,105,107,110}, +["银色护肩"]={300,315,330,345}, +["银色长靴"]={290,305,320,335}, +["银触媒"]={90,110,125,140}, +["银质勋章"]={135,150,160,170}, +["银鳞胸甲"]={145,175,190,205}, +["锋利之爪"]={75,90,95,100}, +["镀银猎枪"]={130,155,167,180}, +["镀银青铜战靴"]={130,160,175,190}, +["镀银青铜护手"]={135,165,180,195}, +["镀银青铜护肩"]={125,155,170,185}, +["镀银青铜护腿"]={155,180,192,205}, +["镀银青铜胸甲"]={130,160,175,190}, +["镶嵌宝石之戒"]={300,330,350,370}, +["镶饰瑟银战锤"]={270,295,307,320}, +["镶饰的铜戒指"]={35,55,62,70}, +["长嘴泥鳅"]={50,90,110,130}, +["长蛇焰火"]={250,250,260,270}, +["闪亮护腕"]={80,100,110,120}, +["闪亮月亮石板"]={195,195,217,240}, +["闪亮水玉"]={150,155,157,160}, +["闪亮青绿石戒指"]={190,210,220,230}, +["闪亮青铜戒指"]={90,105,112,120}, +["闪光的金项链"]={190,205,215,225}, +["闪光的银项链"]={135,155,165,175}, +["闪光雷"]={185,185,205,225}, +["闪光项链"]={80,100,110,120}, +["闪烁的钻戒"]={300,320,330,340}, +["闪耀月亮石指环"]={130,150,160,170}, +["闪耀白玉"]={235,235,237,240}, +["闪耀轻剑"]={280,305,317,330}, +["防御药剂"]={130,155,175,195}, +["阿尔萨斯的礼物"]={240,255,275,295}, +["附魔双手武器 - 冲击"]={200,0,0,0}, +["附魔双手武器 - 初级冲击"]={100,0,0,0}, +["附魔双手武器 - 初级智力"]={75,0,0,0}, +["附魔双手武器 - 强效冲击"]={240,0,0,0}, +["附魔双手武器 - 敏捷"]={290,0,0,0}, +["附魔双手武器 - 极效智力"]={300,0,0,0}, +["附魔双手武器 - 极效精神"]={300,0,0,0}, +["附魔双手武器 - 次级冲击"]={145,0,0,0}, +["附魔双手武器 - 次级智力"]={100,0,0,0}, +["附魔双手武器 - 次级精神"]={110,0,0,0}, +["附魔双手武器 - 超强冲击"]={295,0,0,0}, +["附魔师长靴"]={175,195,210,225}, +["附魔手套 - 冰霜能量"]={300,0,0,0}, +["附魔手套 - 初级加速"]={250,0,0,0}, +["附魔手套 - 剥皮"]={200,0,0,0}, +["附魔手套 - 力量"]={225,0,0,0}, +["附魔手套 - 奥术能量"]={300,0,0,0}, +["附魔手套 - 威胁"]={300,0,0,0}, +["附魔手套 - 强效力量"]={295,0,0,0}, +["附魔手套 - 强效敏捷"]={270,0,0,0}, +["附魔手套 - 敏捷"]={210,0,0,0}, +["附魔手套 - 暗影能量"]={300,0,0,0}, +["附魔手套 - 治疗能量"]={300,0,0,0}, +["附魔手套 - 火焰能量"]={300,0,0,0}, +["附魔手套 - 特效力量"]={300,0,0,0}, +["附魔手套 - 自然之力"]={300,0,0,0}, +["附魔手套 - 草药学"]={145,0,0,0}, +["附魔手套 - 超强敏捷"]={300,0,0,0}, +["附魔手套 - 采矿"]={145,0,0,0}, +["附魔手套 - 钓鱼"]={145,0,0,0}, +["附魔手套 - 骑乘"]={250,0,0,0}, +["附魔手套 - 高级草药学"]={225,0,0,0}, +["附魔手套 - 高级采矿"]={215,0,0,0}, +["附魔护腕 - 偏斜"]={235,0,0,0}, +["附魔护腕 - 初级偏斜"]={1,0,0,0}, +["附魔护腕 - 初级力量"]={80,0,0,0}, +["附魔护腕 - 初级敏捷"]={80,0,0,0}, +["附魔护腕 - 初级生命"]={1,0,0,0}, +["附魔护腕 - 初级精神"]={60,0,0,0}, +["附魔护腕 - 初级耐力"]={50,0,0,0}, +["附魔护腕 - 力量"]={180,0,0,0}, +["附魔护腕 - 吸血鬼"]={185,0,0,0}, +["附魔护腕 - 强效偏斜"]={300,0,0,0}, +["附魔护腕 - 强效力量"]={240,0,0,0}, +["附魔护腕 - 强效敏捷"]={300,0,0,0}, +["附魔护腕 - 强效智力"]={255,0,0,0}, +["附魔护腕 - 强效精神"]={220,0,0,0}, +["附魔护腕 - 强效耐力"]={245,0,0,0}, +["附魔护腕 - 智力"]={210,0,0,0}, +["附魔护腕 - 次级偏斜"]={170,0,0,0}, +["附魔护腕 - 次级力量"]={140,0,0,0}, +["附魔护腕 - 次级智力"]={150,0,0,0}, +["附魔护腕 - 次级精神"]={120,0,0,0}, +["附魔护腕 - 次级耐力"]={130,0,0,0}, +["附魔护腕 - 治疗能量"]={300,0,0,0}, +["附魔护腕 - 法力回复"]={290,0,0,0}, +["附魔护腕 - 法术强度"]={300,0,0,0}, +["附魔护腕 - 精神"]={165,0,0,0}, +["附魔护腕 - 耐力"]={170,0,0,0}, +["附魔护腕 - 超强力量"]={295,0,0,0}, +["附魔护腕 - 超强精神"]={270,0,0,0}, +["附魔护腕 - 超强耐力"]={300,0,0,0}, +["附魔披风 - 初级抗性"]={45,0,0,0}, +["附魔披风 - 初级敏捷"]={110,0,0,0}, +["附魔披风 - 初级防护"]={70,0,0,0}, +["附魔披风 - 强效奥术抗性"]={300,0,0,0}, +["附魔披风 - 强效抗性"]={265,0,0,0}, +["附魔披风 - 强效火焰抗性"]={300,0,0,0}, +["附魔披风 - 强效自然抗性"]={300,0,0,0}, +["附魔披风 - 强效防御"]={205,0,0,0}, +["附魔披风 - 抗性"]={205,0,0,0}, +["附魔披风 - 次级敏捷"]={225,0,0,0}, +["附魔披风 - 次级暗影抵抗"]={135,0,0,0}, +["附魔披风 - 次级火焰抗性"]={125,0,0,0}, +["附魔披风 - 次级防护"]={115,0,0,0}, +["附魔披风 - 潜行"]={300,0,0,0}, +["附魔披风 - 火焰抗性"]={175,0,0,0}, +["附魔披风 - 狡诈"]={300,0,0,0}, +["附魔披风 - 超级防御"]={285,0,0,0}, +["附魔披风 - 躲闪"]={300,0,0,0}, +["附魔披风 - 防御"]={155,0,0,0}, +["附魔武器 - 冰寒"]={285,0,0,0}, +["附魔武器 - 初级屠兽"]={90,0,0,0}, +["附魔武器 - 初级攻击"]={90,0,0,0}, +["附魔武器 - 力量"]={290,0,0,0}, +["附魔武器 - 十字军"]={300,0,0,0}, +["附魔武器 - 寒冬之力"]={190,0,0,0}, +["附魔武器 - 屠魔"]={230,0,0,0}, +["附魔武器 - 强效攻击"]={245,0,0,0}, +["附魔武器 - 强效智力"]={300,0,0,0}, +["附魔武器 - 强效精神"]={300,0,0,0}, +["附魔武器 - 攻击"]={195,0,0,0}, +["附魔武器 - 敏捷"]={290,0,0,0}, +["附魔武器 - 次级元素杀手"]={175,0,0,0}, +["附魔武器 - 次级屠兽"]={175,0,0,0}, +["附魔武器 - 次级攻击"]={140,0,0,0}, +["附魔武器 - 治疗能量"]={300,0,0,0}, +["附魔武器 - 法术能量"]={300,0,0,0}, +["附魔武器 - 烈焰"]={265,0,0,0}, +["附魔武器 - 生命偷取"]={300,0,0,0}, +["附魔武器 - 超强打击"]={300,0,0,0}, +["附魔武器 - 邪恶武器"]={295,0,0,0}, +["附魔盾牌 - 冰霜抗性"]={235,0,0,0}, +["附魔盾牌 - 初级耐力"]={105,0,0,0}, +["附魔盾牌 - 强效精神"]={230,0,0,0}, +["附魔盾牌 - 强效耐力"]={265,0,0,0}, +["附魔盾牌 - 次级格挡"]={195,0,0,0}, +["附魔盾牌 - 次级精神"]={130,0,0,0}, +["附魔盾牌 - 次级耐力"]={155,0,0,0}, +["附魔盾牌 - 次级防护"]={115,0,0,0}, +["附魔盾牌 - 精神"]={180,0,0,0}, +["附魔盾牌 - 耐力"]={210,0,0,0}, +["附魔盾牌 - 超强精神"]={280,0,0,0}, +["附魔胸甲 - 优异法力"]={300,0,0,0}, +["附魔胸甲 - 初级吸收"]={40,0,0,0}, +["附魔胸甲 - 初级属性"]={150,0,0,0}, +["附魔胸甲 - 初级法力"]={20,0,0,0}, +["附魔胸甲 - 初级生命"]={15,0,0,0}, +["附魔胸甲 - 强效属性"]={300,0,0,0}, +["附魔胸甲 - 强效法力"]={185,0,0,0}, +["附魔胸甲 - 强效生命"]={160,0,0,0}, +["附魔胸甲 - 极效法力"]={290,0,0,0}, +["附魔胸甲 - 极效生命"]={275,0,0,0}, +["附魔胸甲 - 次级吸收"]={140,0,0,0}, +["附魔胸甲 - 次级法力"]={80,0,0,0}, +["附魔胸甲 - 次级状态"]={200,0,0,0}, +["附魔胸甲 - 次级生命"]={60,0,0,0}, +["附魔胸甲 - 法力"]={145,0,0,0}, +["附魔胸甲 - 状态"]={245,0,0,0}, +["附魔胸甲 - 生命"]={120,0,0,0}, +["附魔胸甲 - 超强法力"]={230,0,0,0}, +["附魔胸甲 - 超强生命"]={220,0,0,0}, +["附魔靴子 - 初级敏捷"]={125,0,0,0}, +["附魔靴子 - 初级耐力"]={125,0,0,0}, +["附魔靴子 - 初级速度"]={225,0,0,0}, +["附魔靴子 - 吸血鬼"]={300,0,0,0}, +["附魔靴子 - 强效敏捷"]={295,0,0,0}, +["附魔靴子 - 强效精神"]={300,0,0,0}, +["附魔靴子 - 强效耐力"]={260,0,0,0}, +["附魔靴子 - 敏捷"]={235,0,0,0}, +["附魔靴子 - 次级敏捷"]={160,0,0,0}, +["附魔靴子 - 次级智力"]={170,0,0,0}, +["附魔靴子 - 次级精神"]={190,0,0,0}, +["附魔靴子 - 次级耐力"]={170,0,0,0}, +["附魔靴子 - 精神"]={275,0,0,0}, +["附魔靴子 - 耐力"]={215,0,0,0}, +["附魔靴子 - 超强耐力"]={300,0,0,0}, +["附魔靴子-特效智力"]={300,0,0,0}, +["降落伞披风"]={225,245,255,265}, +["隐形药水"]={235,250,270,290}, +["雕花皮外衣"]={40,70,85,100}, +["雕花皮手套"]={55,85,100,115}, +["雕花皮短裤"]={75,105,120,135}, +["雕花皮质披风"]={60,90,105,120}, +["雕花皮靴"]={55,85,100,115}, +["雪王9000型"]={190,0,0,0}, +["雷云徽记"]={290,310,320,330}, +["雷云护腕"]={300,0,0,0}, +["雷云魔印"]={290,310,325,340}, +["雷暴"]={285,305,315,325}, +["雷暴手套"]={300,320,330,340}, +["雷暴护肩"]={295,315,325,335}, +["雷暴短裤"]={275,295,305,315}, +["雷织外衣"]={225,240,255,270}, +["雷织头带"]={240,255,270,285}, +["雷织手套"]={220,235,250,265}, +["雷织护肩"]={245,260,275,290}, +["雷织短裤"]={220,235,250,265}, +["雷织长靴"]={250,265,280,295}, +["雾林头冠"]={105,135,145,155}, +["霜刃外套"]={300,320,330,340}, +["霜刃手套"]={295,315,325,335}, +["霜刃护腿"]={285,305,315,325}, +["霜刃长靴"]={275,295,305,315}, +["霜纹外套"]={255,270,285,300}, +["霜纹手套"]={265,280,295,310}, +["霜纹短裤"]={280,295,310,325}, +["霜纹长袍"]={255,270,285,300}, +["霜虎之刃"]={200,225,237,250}, +["青绿石坠饰"]={215,215,225,235}, +["青铜匕首"]={105,135,150,165}, +["青铜巨剑"]={130,160,175,190}, +["青铜带扣"]={90,115,127,140}, +["青铜战斧"]={135,165,180,195}, +["青铜战锤"]={125,155,170,185}, +["青铜手镯"]={105,120,132,145}, +["青铜斧"]={115,145,160,175}, +["青铜权杖"]={110,130,140,150}, +["青铜框架"]={145,145,170,195}, +["青铜短剑"]={120,150,165,180}, +["青铜管"]={105,105,130,155}, +["青铜花纹护腕"]={120,150,165,180}, +["青铜荣耀者"]={120,140,145,150}, +["青铜重锤"]={130,160,175,190}, +["青铜钉锤"]={110,140,155,170}, +["韧化皮手套"]={135,160,172,185}, +["韧化皮甲"]={120,145,157,170}, +["预视之眼"]={285,320,330,340}, +["预言家帽子"]={205,205,205,205}, +["预言家手套"]={200,200,200,200}, +["预言家护肩"]={205,205,205,205}, +["预言家裤子"]={205,205,205,205}, +["预言家长袍"]={210,210,210,210}, +["预言家靴子"]={200,200,200,200}, +["风之靴"]={255,255,255,255}, +["风暴护手"]={295,315,325,335}, +["风暴鳞片护腿"]={300,300,300,300}, +["风行者之靴"]={300,300,300,300}, +["飞火护手"]={300,320,330,340}, +["飞火护腕"]={300,320,330,340}, +["飞火胸甲"]={300,320,330,340}, +["飞虎护目镜"]={100,130,145,160}, +["食人魔力量药剂"]={150,175,195,215}, +["食人魔白骨指环"]={170,200,210,220}, +["首领头饰"]={270,290,291,295}, +["首领手套"]={270,285,290,295}, +["首领护肩"]={280,285,290,295}, +["首领短裤"]={280,290,291,295}, +["首领长靴"]={275,290,291,295}, +["首领马甲"]={285,290,291,295}, +["香烤狮肉"]={150,190,210,230}, +["香脆蝙蝠翅"]={1,45,65,85}, +["香辣狼肉"]={10,50,70,90}, +["骗子兜帽"]={210,225,230,235}, +["骗子外衣"]={210,235,240,245}, +["骗子手套"]={200,215,220,225}, +["骗子护腿"]={205,225,230,235}, +["骗子腰带"]={200,215,220,225}, +["骗子靴子"]={200,215,220,225}, +["高爆炸弹"]={235,235,255,275}, +["高级亚麻手套"]={35,60,77,95}, +["高级假人"]={185,185,205,225}, +["高级技师护目镜"]={290,310,320,330}, +["高级毛纺手套"]={85,110,127,145}, +["高级毛纺披风"]={100,125,142,160}, +["高级毛纺短裤"]={110,135,152,170}, +["高级活动假人"]={275,295,305,315}, +["高辐射烈焰反射器"]={290,310,320,330}, +["高速秘银弹头"]={210,210,230,250}, +["高速青铜齿轮"]={125,125,150,175}, +["高雅之冠"]={230,260,270,280}, +["鬼纹外衣"]={275,290,305,320}, +["鬼纹手套"]={270,285,300,315}, +["鬼纹短裤"]={290,305,320,335}, +["鬼纹腰带"]={265,280,295,310}, +["魔化战锤"]={280,305,317,330}, +["魔化手镯"]={125,145,155,165}, +["魔化护甲片"]={300,320,330,340}, +["魔化瑟银头盔"]={300,320,330,340}, +["魔化瑟银带扣"]={285,285,297,310}, +["魔化瑟银护腿"]={300,320,330,340}, +["魔化瑟银胸甲"]={300,320,330,340}, +["魔化瑟银锭"]={250,0,0,0}, +["魔化皮"]={250,0,0,0}, +["魔化符文布包"]={275,290,305,320}, +["魔化翡翠"]={250,255,257,260}, +["魔化魔纹布包"]={225,240,255,270}, +["魔导力量之杖"]={300,325,335,345}, +["魔暴龙皮手套"]={290,310,320,330}, +["魔暴龙皮护腿"]={300,320,330,340}, +["魔法宝石油"]={275,0,0,0}, +["魔法禁锢徽记"]={230,260,275,290}, +["魔符雕刻护腕"]={300,320,330,340}, +["魔纹包"]={225,240,255,270}, +["魔纹布卷"]={175,180,182,185}, +["魔纹绷带"]={210,210,240,270}, +["魔网亲和披风"]={300,300,300,300}, +["魔血药水"]={275,290,310,330}, +["魔铸胸甲"]={285,305,315,325}, +["鱼人鳍汤"]={90,130,150,170}, +["鱼人鳞片护腕"]={190,210,220,230}, +["鱼人鳞片胸甲"]={95,125,140,155}, +["鱼人鳞片腰带"]={90,120,135,150}, +["鳄鱼浓汤"]={120,160,180,200}, +["鳄鱼炖肉"]={150,160,180,200}, +["鳄鱼肉排"]={80,120,140,160}, +["麻痹毒药"]={1,150,175,200}, +["麻痹毒药 II"]={1,215,240,265}, +["麻痹毒药 III"]={1,285,310,335}, +["黄玉坠饰"]={160,160,180,200}, +["黄金万能钥匙"]={150,150,160,170}, +["黄金权杖"]={260,0,0,0}, +["黄金石英头冠"]={175,195,202,210}, +["黄金符文戒指"]={285,310,320,330}, +["黄金能量核心"]={150,150,170,190}, +["黄金酒杯"]={175,195,202,210}, +["黎明之刃"]={275,300,312,325}, +["黎明使者护肩"]={290,310,320,330}, +["黎明手套"]={300,320,330,340}, +["黎明束腰"]={290,310,320,330}, +["黎明皮靴"]={290,310,320,330}, +["黎明石锤"]={300,325,337,350}, +["黑丝衬衣"]={155,165,170,175}, +["黑口鱼油"]={80,80,90,100}, +["黑手"]={300,320,330,340}, +["黑暗之手"]={145,165,180,195}, +["黑暗之矛"]={300,320,330,340}, +["黑暗之靴"]={140,160,175,190}, +["黑暗之魂护肩"]={300,320,330,340}, +["黑暗之魂护腿"]={300,320,330,340}, +["黑暗之魂胸甲"]={300,320,330,340}, +["黑暗符文头盔"]={300,320,330,340}, +["黑暗符文护手"]={300,320,330,340}, +["黑暗符文胸甲"]={300,320,330,340}, +["黑曜石带扣"]={300,300,310,320}, +["黑曜石胸针"]={175,190,197,205}, +["黑曜石锁甲"]={300,320,330,340}, +["黑皮外套"]={100,125,137,150}, +["黑皮战靴"]={100,125,137,150}, +["黑皮手套"]={120,155,167,180}, +["黑皮护肩"]={140,165,177,190}, +["黑皮披风"]={110,135,147,160}, +["黑皮短裤"]={115,140,152,165}, +["黑皮腰带"]={125,150,162,175}, +["黑石铁夹"]={140,160,170,180}, +["黑色丝质背包"]={185,205,220,235}, +["黑色冒险者衬衣"]={200,210,215,220}, +["黑色卫士"]={300,320,330,340}, +["黑色怒火"]={300,320,330,340}, +["黑色雏龙外衣"]={100,125,137,150}, +["黑色雏龙披风"]={100,125,137,150}, +["黑色魔纹之靴"]={230,245,260,275}, +["黑色魔纹外衣"]={205,220,235,250}, +["黑色魔纹头带"]={230,245,260,275}, +["黑色魔纹手套"]={215,230,245,260}, +["黑色魔纹护肩"]={230,245,260,275}, +["黑色魔纹短裤"]={205,220,235,250}, +["黑色魔纹长袍"]={210,225,240,255}, +["黑色龙鳞战靴"]={300,320,330,340}, +["黑色龙鳞护肩"]={300,320,330,340}, +["黑色龙鳞护腿"]={300,320,330,340}, +["黑色龙鳞胸甲"]={290,310,320,330}, +["黑铁利剑"]={300,320,330,340}, +["黑铁头盔"]={300,320,330,340}, +["黑铁带扣"]={275,275,280,285}, +["黑铁徽记之戒"]={290,320,330,340}, +["黑铁战斧"]={300,320,330,340}, +["黑铁护手"]={300,320,330,340}, +["黑铁护肩"]={280,300,310,320}, +["黑铁护腕"]={295,315,325,335}, +["黑铁护腿"]={300,320,330,340}, +["黑铁斩碎者"]={275,295,305,315}, +["黑铁板甲"]={285,305,315,325}, +["黑铁步枪"]={275,295,305,315}, +["黑铁炸弹"]={285,305,315,325}, +["黑铁矮人伪装"]={1,0,0,0}, +["黑铁粉碎者"]={265,285,295,305}, +["黑铁锁甲"]={270,290,300,310}, +["黑铁长靴"]={300,320,330,340}, +["龙喉手套"]={170,170,182,195}, +["龙喉护甲片"]={175,175,182,190}, +["龙息红椒"]={200,240,260,280}, +["龙鳞带扣"]={235,235,240,245}, +["龙鳞护手"]={225,245,255,265}, +["龙鳞护腿"]={245,245,245,245}, +["龙鳞胸甲"]={255,275,285,295}, +["龟壳头盔"]={230,250,260,270}, +["龟壳手套"]={205,225,235,245}, +["龟壳护腕"]={210,230,240,250}, +["龟壳护腿"]={235,255,265,275}, +["龟壳胸甲"]={210,230,240,250}, +} + +-- T=Trainer, A=AutoLearned, Q=Quest, D=Drop, V=Vendor, F=Fishing, O=WorldObject, ?=Unknown +NanamiTradeSkillSources = { +[65]="D", +[66]="D", +[67]="D", +[82]="V", +[87]="D", +[103]="D", +[118]="A", +[131]="D", +[151]="D", +[156]="D", +[724]="V", +[733]="V", +[787]="V", +[858]="T", +[929]="T", +[1017]="V", +[1082]="V", +[1251]="A", +[1710]="T", +[2300]="T", +[2302]="A", +[2303]="T", +[2304]="A", +[2307]="D", +[2308]="T", +[2309]="T", +[2310]="T", +[2311]="D", +[2312]="D", +[2313]="T", +[2314]="T", +[2315]="T", +[2316]="T", +[2317]="D", +[2318]="A", +[2319]="T", +[2454]="A", +[2455]="T", +[2456]="T", +[2457]="D", +[2458]="T", +[2459]="D", +[2568]="T", +[2569]="T", +[2570]="A", +[2572]="D", +[2575]="T", +[2576]="T", +[2577]="T", +[2578]="T", +[2579]="T", +[2580]="T", +[2581]="T", +[2582]="T", +[2583]="T", +[2584]="T", +[2585]="D", +[2587]="T", +[2679]="A", +[2680]="T", +[2681]="A", +[2682]="D", +[2683]="T", +[2684]="T", +[2685]="D", +[2687]="T", +[2840]="A", +[2841]="T", +[2842]="T", +[2844]="T", +[2845]="T", +[2847]="T", +[2848]="T", +[2849]="T", +[2850]="T", +[2851]="T", +[2852]="T", +[2853]="A", +[2854]="T", +[2857]="T", +[2862]="A", +[2863]="T", +[2864]="D", +[2865]="T", +[2866]="T", +[2867]="V", +[2868]="T", +[2869]="D", +[2870]="T", +[2871]="T", +[2888]="V", +[2892]="T", +[2893]="T", +[2996]="A", +[2997]="T", +[3220]="V", +[3239]="A", +[3240]="T", +[3241]="T", +[3382]="T", +[3383]="T", +[3384]="D", +[3385]="T", +[3386]="D", +[3387]="D", +[3388]="T", +[3389]="T", +[3390]="D", +[3391]="D", +[3469]="T", +[3470]="T", +[3471]="D", +[3472]="T", +[3473]="T", +[3474]="D", +[3478]="T", +[3480]="T", +[3481]="D", +[3482]="T", +[3483]="T", +[3484]="D", +[3485]="D", +[3486]="T", +[3487]="T", +[3488]="T", +[3489]="T", +[3490]="D", +[3491]="T", +[3492]="D", +[3530]="T", +[3531]="T", +[3575]="T", +[3576]="T", +[3577]="V", +[3662]="V", +[3663]="V", +[3664]="V", +[3665]="V", +[3666]="V", +[3719]="T", +[3726]="V", +[3727]="V", +[3728]="Q", +[3729]="Q", +[3775]="T", +[3776]="T", +[3823]="T", +[3824]="V", +[3825]="D", +[3826]="D", +[3827]="T", +[3828]="D", +[3829]="V", +[3835]="T", +[3836]="T", +[3837]="V", +[3840]="D", +[3841]="D", +[3842]="T", +[3843]="D", +[3844]="T", +[3845]="D", +[3846]="D", +[3847]="D", +[3848]="T", +[3849]="V", +[3850]="D", +[3851]="V", +[3852]="D", +[3853]="V", +[3854]="D", +[3855]="V", +[3856]="D", +[3859]="T", +[3860]="T", +[3928]="T", +[4231]="T", +[4233]="T", +[4234]="T", +[4236]="T", +[4237]="T", +[4238]="T", +[4239]="T", +[4240]="T", +[4241]="D", +[4242]="T", +[4243]="T", +[4244]="D", +[4245]="T", +[4246]="T", +[4247]="T", +[4248]="D", +[4249]="T", +[4250]="D", +[4251]="T", +[4252]="D", +[4253]="T", +[4254]="D", +[4255]="V", +[4256]="D", +[4257]="T", +[4258]="D", +[4259]="T", +[4260]="D", +[4262]="V", +[4264]="D", +[4265]="T", +[4304]="T", +[4305]="T", +[4307]="T", +[4308]="T", +[4309]="T", +[4310]="T", +[4311]="D", +[4312]="T", +[4313]="D", +[4314]="T", +[4315]="D", +[4316]="T", +[4317]="D", +[4318]="T", +[4319]="V", +[4320]="T", +[4321]="D", +[4322]="V", +[4323]="D", +[4324]="T", +[4325]="D", +[4326]="T", +[4327]="V", +[4328]="D", +[4329]="D", +[4330]="T", +[4331]="D", +[4332]="V", +[4333]="V", +[4334]="T", +[4335]="D", +[4336]="V", +[4339]="T", +[4343]="T", +[4344]="A", +[4357]="A", +[4358]="A", +[4359]="T", +[4360]="T", +[4361]="T", +[4362]="T", +[4363]="T", +[4364]="T", +[4365]="T", +[4366]="T", +[4367]="D", +[4368]="T", +[4369]="T", +[4370]="T", +[4371]="T", +[4372]="V", +[4373]="D", +[4374]="T", +[4375]="T", +[4376]="D", +[4377]="T", +[4378]="T", +[4379]="T", +[4380]="T", +[4381]="V", +[4382]="T", +[4383]="D", +[4384]="T", +[4385]="T", +[4386]="V", +[4387]="T", +[4388]="D", +[4389]="T", +[4390]="T", +[4391]="T", +[4392]="T", +[4393]="D", +[4394]="T", +[4395]="D", +[4396]="V", +[4397]="D", +[4398]="D", +[4401]="D", +[4403]="D", +[4404]="T", +[4405]="T", +[4406]="T", +[4407]="V", +[4455]="V", +[4456]="V", +[4457]="V", +[4592]="V", +[4593]="V", +[4594]="V", +[4596]="Q", +[4623]="Q", +[4852]="Q", +[5081]="Q", +[5095]="V", +[5237]="T", +[5472]="Q", +[5473]="V", +[5474]="V", +[5476]="V", +[5477]="V", +[5478]="Q", +[5479]="V", +[5480]="V", +[5507]="T", +[5525]="T", +[5526]="V", +[5527]="T", +[5530]="T", +[5540]="T", +[5541]="D", +[5542]="T", +[5631]="V", +[5633]="V", +[5634]="V", +[5739]="T", +[5762]="D", +[5763]="D", +[5764]="D", +[5765]="D", +[5766]="T", +[5770]="D", +[5780]="D", +[5781]="D", +[5782]="D", +[5783]="D", +[5957]="A", +[5958]="D", +[5961]="T", +[5962]="T", +[5963]="V", +[5964]="T", +[5965]="D", +[5966]="T", +[5996]="T", +[5997]="A", +[6037]="V", +[6038]="V", +[6040]="T", +[6041]="D", +[6042]="D", +[6043]="D", +[6048]="V", +[6049]="V", +[6050]="V", +[6051]="V", +[6052]="V", +[6149]="T", +[6214]="T", +[6218]="A", +[6219]="T", +[6238]="T", +[6239]="D", +[6240]="V", +[6241]="T", +[6242]="V", +[6243]="V", +[6263]="V", +[6264]="V", +[6290]="V", +[6316]="V", +[6338]="T", +[6339]="T", +[6350]="T", +[6370]="T", +[6371]="T", +[6372]="T", +[6373]="T", +[6384]="D", +[6385]="D", +[6450]="T", +[6451]="V", +[6452]="T", +[6453]="D", +[6466]="V", +[6467]="V", +[6468]="Q", +[6533]="T", +[6657]="D", +[6662]="D", +[6709]="Q", +[6712]="T", +[6714]="D", +[6730]="V", +[6731]="Q", +[6733]="V", +[6786]="T", +[6787]="T", +[6795]="T", +[6796]="T", +[6887]="V", +[6888]="A", +[6890]="V", +[6947]="A", +[6949]="T", +[6950]="T", +[6951]="T", +[7026]="T", +[7027]="V", +[7046]="T", +[7047]="D", +[7048]="T", +[7049]="D", +[7050]="T", +[7051]="T", +[7052]="T", +[7053]="V", +[7054]="T", +[7055]="T", +[7056]="V", +[7057]="T", +[7058]="T", +[7059]="D", +[7060]="D", +[7061]="D", +[7062]="T", +[7063]="V", +[7064]="T", +[7065]="D", +[7067]="V", +[7068]="V", +[7070]="V", +[7071]="T", +[7076]="V", +[7078]="V", +[7080]="V", +[7082]="V", +[7148]="V", +[7166]="T", +[7189]="T", +[7276]="A", +[7277]="A", +[7278]="T", +[7279]="T", +[7280]="D", +[7281]="T", +[7282]="T", +[7283]="V", +[7284]="V", +[7285]="T", +[7348]="T", +[7349]="V", +[7352]="V", +[7358]="D", +[7359]="D", +[7371]="T", +[7372]="T", +[7373]="D", +[7374]="T", +[7375]="D", +[7377]="T", +[7378]="T", +[7386]="V", +[7387]="T", +[7390]="D", +[7391]="D", +[7506]="V", +[7676]="V", +[7913]="Q", +[7914]="Q", +[7915]="Q", +[7916]="Q", +[7917]="Q", +[7918]="T", +[7919]="T", +[7920]="T", +[7921]="D", +[7922]="T", +[7924]="V", +[7925]="V", +[7926]="Q", +[7927]="Q", +[7928]="Q", +[7929]="Q", +[7930]="T", +[7931]="T", +[7932]="D", +[7933]="T", +[7934]="D", +[7935]="Q", +[7936]="Q", +[7937]="Q", +[7938]="T", +[7939]="T", +[7941]="T", +[7942]="D", +[7943]="D", +[7944]="D", +[7945]="T", +[7946]="D", +[7947]="V", +[7954]="T", +[7955]="T", +[7956]="T", +[7957]="T", +[7958]="T", +[7959]="T", +[7960]="T", +[7961]="T", +[7963]="T", +[7964]="T", +[7965]="T", +[7966]="T", +[7967]="D", +[7969]="D", +[8067]="A", +[8068]="T", +[8069]="T", +[8170]="T", +[8172]="T", +[8173]="T", +[8174]="D", +[8175]="T", +[8176]="T", +[8185]="T", +[8187]="D", +[8189]="T", +[8191]="T", +[8192]="V", +[8193]="T", +[8195]="V", +[8197]="T", +[8198]="T", +[8200]="D", +[8201]="D", +[8202]="D", +[8203]="D", +[8204]="D", +[8205]="D", +[8206]="D", +[8207]="D", +[8208]="D", +[8209]="D", +[8210]="Q", +[8211]="Q", +[8212]="Q", +[8213]="Q", +[8214]="Q", +[8215]="Q", +[8216]="D", +[8217]="T", +[8218]="T", +[8345]="T", +[8346]="T", +[8347]="T", +[8348]="T", +[8349]="T", +[8364]="V", +[8367]="T", +[8544]="V", +[8545]="T", +[8546]="D", +[8926]="T", +[8927]="T", +[8928]="T", +[8949]="T", +[8951]="T", +[8956]="T", +[8984]="T", +[8985]="T", +[9030]="Q", +[9036]="D", +[9060]="E", +[9061]="E", +[9088]="D", +[9144]="D", +[9149]="V", +[9154]="T", +[9155]="T", +[9172]="D", +[9179]="T", +[9186]="T", +[9187]="T", +[9197]="D", +[9206]="D", +[9210]="V", +[9224]="V", +[9233]="T", +[9264]="V", +[9312]="V", +[9313]="V", +[9318]="V", +[9366]="Q", +[9998]="T", +[9999]="T", +[10001]="T", +[10002]="T", +[10003]="T", +[10004]="T", +[10007]="D", +[10008]="D", +[10009]="D", +[10010]="D", +[10011]="D", +[10018]="D", +[10019]="T", +[10020]="D", +[10021]="T", +[10023]="T", +[10024]="T", +[10025]="Q", +[10026]="T", +[10027]="T", +[10028]="T", +[10029]="D", +[10030]="V", +[10031]="T", +[10032]="D", +[10033]="D", +[10034]="V", +[10035]="V", +[10036]="V", +[10038]="D", +[10039]="D", +[10040]="V", +[10041]="T", +[10042]="T", +[10044]="T", +[10045]="A", +[10046]="T", +[10047]="T", +[10048]="D", +[10050]="T", +[10051]="T", +[10052]="V", +[10053]="T", +[10054]="V", +[10055]="V", +[10056]="T", +[10421]="A", +[10423]="D", +[10498]="T", +[10499]="D", +[10500]="T", +[10501]="D", +[10502]="D", +[10503]="T", +[10504]="T", +[10505]="T", +[10506]="V", +[10507]="T", +[10508]="T", +[10510]="D", +[10512]="T", +[10513]="T", +[10514]="T", +[10518]="D", +[10542]="T", +[10543]="T", +[10545]="T", +[10546]="V", +[10548]="D", +[10558]="T", +[10559]="T", +[10560]="T", +[10561]="T", +[10562]="T", +[10576]="V", +[10577]="T", +[10585]="V", +[10586]="T", +[10587]="T", +[10588]="T", +[10592]="T", +[10644]="T", +[10645]="T", +[10646]="T", +[10713]="T", +[10716]="T", +[10720]="T", +[10721]="T", +[10724]="T", +[10725]="T", +[10726]="T", +[10727]="T", +[10841]="T", +[10918]="T", +[10920]="T", +[10921]="T", +[10922]="T", +[11128]="T", +[11130]="T", +[11144]="T", +[11145]="T", +[11287]="T", +[11288]="T", +[11289]="T", +[11290]="T", +[11371]="Q", +[11590]="T", +[11604]="D", +[11605]="D", +[11606]="D", +[11607]="D", +[11608]="D", +[11811]="D", +[11825]="G", +[11826]="G", +[12190]="T", +[12209]="V", +[12210]="V", +[12212]="V", +[12213]="V", +[12214]="V", +[12215]="V", +[12216]="V", +[12217]="V", +[12218]="V", +[12224]="V", +[12259]="T", +[12260]="D", +[12359]="T", +[12360]="V", +[12404]="T", +[12405]="D", +[12406]="D", +[12408]="D", +[12409]="D", +[12410]="D", +[12414]="D", +[12415]="D", +[12416]="D", +[12417]="D", +[12418]="D", +[12419]="D", +[12420]="D", +[12422]="Q", +[12424]="Q", +[12425]="Q", +[12426]="Q", +[12427]="Q", +[12428]="Q", +[12429]="Q", +[12610]="D", +[12611]="D", +[12612]="D", +[12613]="D", +[12614]="D", +[12618]="Q", +[12619]="Q", +[12620]="Q", +[12624]="D", +[12625]="D", +[12628]="Q", +[12631]="Q", +[12632]="D", +[12633]="D", +[12636]="D", +[12639]="D", +[12640]="D", +[12641]="D", +[12643]="T", +[12644]="T", +[12645]="D", +[12655]="T", +[12764]="V", +[12769]="V", +[12772]="V", +[12773]="V", +[12774]="Q", +[12775]="V", +[12776]="Q", +[12777]="Q", +[12779]="V", +[12781]="D", +[12782]="D", +[12783]="D", +[12784]="D", +[12790]="D", +[12792]="D", +[12794]="D", +[12795]="V", +[12796]="D", +[12797]="D", +[12798]="D", +[12802]="V", +[12803]="D", +[12808]="D", +[12810]="T", +[13423]="T", +[13442]="D", +[13443]="V", +[13444]="D", +[13445]="V", +[13446]="V", +[13447]="D", +[13452]="D", +[13453]="Q", +[13454]="D", +[13455]="D", +[13456]="D", +[13457]="D", +[13458]="D", +[13459]="D", +[13460]="V", +[13461]="D", +[13462]="D", +[13503]="D", +[13506]="D", +[13510]="D", +[13511]="D", +[13512]="D", +[13513]="D", +[13851]="V", +[13856]="T", +[13857]="D", +[13858]="V", +[13860]="V", +[13863]="V", +[13864]="V", +[13865]="D", +[13866]="D", +[13867]="D", +[13868]="D", +[13869]="D", +[13870]="D", +[13871]="D", +[13927]="V", +[13928]="V", +[13929]="V", +[13930]="V", +[13931]="V", +[13932]="V", +[13933]="V", +[13934]="V", +[13935]="V", +[14042]="D", +[14043]="D", +[14044]="D", +[14045]="D", +[14046]="V", +[14048]="T", +[14100]="D", +[14101]="D", +[14103]="D", +[14104]="D", +[14106]="D", +[14107]="V", +[14108]="D", +[14111]="D", +[14112]="D", +[14128]="D", +[14130]="D", +[14132]="D", +[14134]="D", +[14136]="D", +[14137]="D", +[14138]="D", +[14139]="D", +[14140]="D", +[14141]="D", +[14142]="D", +[14143]="D", +[14144]="D", +[14146]="D", +[14152]="D", +[14153]="D", +[14154]="D", +[14155]="D", +[14156]="D", +[14342]="V", +[14529]="T", +[14530]="T", +[15045]="V", +[15046]="D", +[15047]="D", +[15048]="V", +[15049]="D", +[15050]="V", +[15051]="D", +[15052]="D", +[15053]="D", +[15054]="D", +[15055]="D", +[15056]="D", +[15057]="V", +[15058]="D", +[15059]="D", +[15060]="D", +[15061]="V", +[15062]="D", +[15063]="V", +[15064]="D", +[15065]="D", +[15066]="D", +[15067]="V", +[15068]="D", +[15069]="D", +[15070]="D", +[15071]="V", +[15072]="D", +[15073]="D", +[15074]="V", +[15075]="D", +[15076]="D", +[15077]="V", +[15078]="D", +[15079]="D", +[15080]="V", +[15081]="D", +[15082]="D", +[15083]="V", +[15084]="D", +[15085]="D", +[15086]="D", +[15087]="D", +[15088]="D", +[15090]="D", +[15091]="D", +[15092]="D", +[15093]="D", +[15094]="V", +[15095]="D", +[15096]="D", +[15138]="Q", +[15141]="?", +[15407]="T", +[15564]="T", +[15802]="Q", +[15846]="T", +[15869]="T", +[15870]="T", +[15871]="T", +[15872]="T", +[15992]="T", +[15993]="V", +[15994]="V", +[15995]="D", +[15996]="D", +[15997]="D", +[15999]="D", +[16000]="V", +[16004]="D", +[16005]="D", +[16006]="V", +[16007]="D", +[16008]="D", +[16009]="D", +[16022]="D", +[16023]="V", +[16040]="D", +[16206]="T", +[16207]="V", +[16766]="V", +[16979]="V", +[16980]="V", +[16982]="V", +[16983]="V", +[16984]="V", +[16988]="V", +[16989]="V", +[17013]="V", +[17014]="V", +[17015]="V", +[17016]="V", +[17193]="Q", +[17197]="V", +[17198]="V", +[17222]="T", +[17704]="D", +[17708]="Q", +[17716]="Q", +[17721]="D", +[17723]="Q", +[17771]="T", +[17967]="Q", +[17968]="Q", +[18045]="V", +[18168]="D", +[18232]="O", +[18238]="V", +[18251]="D", +[18253]="D", +[18254]="D", +[18258]="Q", +[18262]="D", +[18263]="D", +[18282]="D", +[18283]="D", +[18294]="T", +[18405]="D", +[18407]="D", +[18408]="D", +[18409]="D", +[18413]="D", +[18486]="V", +[18504]="D", +[18506]="D", +[18508]="D", +[18509]="D", +[18510]="D", +[18511]="D", +[18587]="D", +[18588]="V", +[18594]="V", +[18631]="V", +[18634]="V", +[18637]="D", +[18638]="D", +[18639]="D", +[18641]="T", +[18645]="D", +[18660]="D", +[18662]="V", +[18948]="V", +[18984]="T", +[18986]="T", +[19026]="V", +[19043]="V", +[19044]="V", +[19047]="V", +[19048]="V", +[19049]="V", +[19050]="V", +[19051]="V", +[19052]="V", +[19056]="V", +[19057]="V", +[19058]="V", +[19059]="V", +[19148]="V", +[19149]="V", +[19156]="V", +[19157]="V", +[19162]="V", +[19163]="V", +[19164]="V", +[19165]="V", +[19166]="V", +[19167]="V", +[19168]="V", +[19169]="V", +[19170]="V", +[19440]="V", +[19682]="V", +[19683]="V", +[19684]="V", +[19685]="V", +[19686]="V", +[19687]="V", +[19688]="V", +[19689]="V", +[19690]="V", +[19691]="V", +[19692]="V", +[19693]="V", +[19694]="V", +[19695]="V", +[19931]="O", +[19998]="V", +[19999]="V", +[20002]="V", +[20004]="V", +[20007]="V", +[20008]="V", +[20039]="V", +[20074]="V", +[20295]="T", +[20296]="T", +[20380]="V", +[20452]="Q", +[20476]="V", +[20477]="V", +[20478]="V", +[20479]="V", +[20480]="V", +[20481]="V", +[20537]="Q", +[20538]="D", +[20539]="Q", +[20549]="Q", +[20550]="Q", +[20551]="Q", +[20575]="V", +[20744]="V", +[20745]="V", +[20746]="V", +[20747]="V", +[20748]="V", +[20749]="V", +[20750]="V", +[20844]="D", +[21023]="Q", +[21072]="V", +[21154]="Q", +[21217]="V", +[21277]="Q", +[21278]="D", +[21340]="V", +[21341]="O", +[21342]="D", +[21542]="Q", +[21546]="D", +[21557]="Q", +[21558]="Q", +[21559]="Q", +[21569]="Q", +[21570]="Q", +[21571]="Q", +[21574]="Q", +[21576]="Q", +[21589]="Q", +[21590]="Q", +[21592]="Q", +[21714]="Q", +[21716]="Q", +[21718]="Q", +[22191]="V", +[22194]="D", +[22195]="V", +[22196]="D", +[22197]="V", +[22198]="V", +[22246]="V", +[22248]="V", +[22249]="D", +[22251]="V", +[22252]="V", +[22383]="D", +[22384]="D", +[22385]="D", +[22652]="T", +[22654]="T", +[22655]="T", +[22658]="T", +[22660]="V", +[22661]="T", +[22662]="T", +[22663]="T", +[22664]="T", +[22665]="T", +[22666]="T", +[22669]="T", +[22670]="T", +[22671]="T", +[22756]="V", +[22757]="V", +[22758]="V", +[22759]="V", +[22760]="V", +[22761]="V", +[22762]="V", +[22763]="V", +[22764]="V", +[30818]="Q", +[41308]="T", +[41309]="T", +[41310]="T", +[41311]="T", +[41312]="T", +[41313]="T", +[41314]="T", +[41315]="T", +[41316]="T", +[41318]="T", +[41319]="T", +[41320]="T", +[41321]="T", +[41322]="T", +[41323]="T", +[41324]="?", +[41325]="T", +[41326]="V", +[41327]="D", +[41328]="D", +[41329]="T", +[41330]="?", +[41331]="T", +[41332]="T", +[41340]="T", +[41341]="T", +[41342]="T", +[41343]="T", +[41344]="T", +[41345]="T", +[41346]="T", +[41347]="T", +[41349]="T", +[41673]="?", +[41674]="V", +[46600]="Q", +[47408]="T", +[47409]="D", +[47410]="Q", +[47412]="D", +[47414]="D", +[50237]="D", +[51256]="D", +[51262]="?", +[51264]="D", +[51268]="D", +[51284]="D", +[51312]="V", +[51313]="V", +[53015]="Q", +[54009]="T", +[54010]="T", +[55043]="V", +[55046]="V", +[55048]="V", +[55050]="V", +[55052]="V", +[55054]="V", +[55056]="V", +[55058]="V", +[55060]="V", +[55141]="T", +[55142]="T", +[55143]="T", +[55144]="T", +[55145]="T", +[55146]="T", +[55147]="T", +[55148]="T", +[55150]="A", +[55151]="T", +[55152]="T", +[55153]="T", +[55154]="T", +[55156]="A", +[55157]="A", +[55158]="T", +[55159]="T", +[55160]="T", +[55161]="T", +[55162]="T", +[55163]="T", +[55164]="D", +[55165]="T", +[55166]="T", +[55167]="T", +[55168]="T", +[55169]="D", +[55170]="T", +[55171]="T", +[55172]="T", +[55173]="T", +[55174]="T", +[55175]="T", +[55176]="T", +[55178]="?", +[55180]="D", +[55195]="?", +[55196]="T", +[55197]="D", +[55198]="?", +[55199]="D", +[55200]="D", +[55202]="D", +[55204]="D", +[55210]="D", +[55211]="D", +[55212]="D", +[55213]="D", +[55228]="D", +[55241]="?", +[55242]="D", +[55243]="?", +[55244]="D", +[55248]="V", +[55255]="V", +[55256]="?", +[55258]="V", +[55259]="D", +[55260]="?", +[55261]="?", +[55263]="?", +[55264]="?", +[55265]="D", +[55266]="D", +[55267]="?", +[55268]="?", +[55269]="?", +[55271]="?", +[55272]="D", +[55273]="?", +[55316]="D", +[55317]="D", +[55318]="D", +[55319]="V", +[55320]="D", +[55321]="D", +[55322]="D", +[55323]="D", +[55324]="D", +[55325]="D", +[55326]="D", +[55327]="D", +[55328]="D", +[55329]="D", +[55330]="D", +[55331]="D", +[55332]="D", +[55333]="D", +[55334]="D", +[55335]="D", +[55336]="D", +[55337]="D", +[55338]="?", +[55339]="?", +[55340]="D", +[55341]="?", +[55359]="D", +[55360]="D", +[55361]="D", +[55362]="V", +[55363]="D", +[55364]="D", +[55365]="D", +[55366]="D", +[55367]="D", +[55368]="D", +[55518]="Q", +[55519]="Q", +[55520]="Q", +[55521]="Q", +[55522]="Q", +[55523]="Q", +[55524]="Q", +[55525]="Q", +[55526]="Q", +[55527]="Q", +[55528]="Q", +[55529]="Q", +[55530]="Q", +[55531]="Q", +[55532]="Q", +[55533]="Q", +[55534]="Q", +[56000]="T", +[56001]="T", +[56002]="Q", +[56003]="D", +[56004]="Q", +[56005]="T", +[56006]="Q", +[56007]="?", +[56008]="T", +[56009]="?", +[56010]="D", +[56011]="?", +[56012]="D", +[56013]="D", +[56014]="D", +[56015]="D", +[56016]="D", +[56017]="D", +[56018]="D", +[56019]="T", +[56020]="T", +[56023]="D", +[56031]="D", +[56032]="D", +[56033]="?", +[56034]="D", +[56035]="D", +[56036]="?", +[56037]="D", +[56038]="D", +[56039]="D", +[56040]="D", +[56041]="D", +[56042]="D", +[56043]="V", +[56044]="D", +[56045]="D", +[56046]="D", +[56047]="Q", +[56048]="Q", +[56049]="Q", +[56050]="Q", +[56051]="Q", +[56052]="Q", +[56053]="D", +[56054]="Q", +[56055]="Q", +[56056]="T", +[56057]="?", +[56058]="V", +[56059]="D", +[56060]="D", +[56061]="D", +[56062]="D", +[56063]="D", +[56064]="Q", +[56065]="?", +[56066]="D", +[56067]="V", +[56068]="D", +[56069]="D", +[56070]="Q", +[56071]="Q", +[56072]="D", +[56073]="D", +[56074]="D", +[56075]="D", +[56076]="D", +[56077]="D", +[56089]="D", +[56090]="D", +[56091]="D", +[56092]="T", +[56093]="D", +[56094]="D", +[56095]="D", +[56096]="Q", +[56112]="D", +[56113]="D", +[58112]="D", +[58134]="D", +[58304]="D", +[58305]="D", +[60007]="D", +[60008]="D", +[60009]="D", +[60010]="D", +[60098]="D", +[60099]="D", +[60287]="D", +[60288]="D", +[60289]="D", +[60290]="D", +[60291]="D", +[60292]="D", +[60293]="D", +[60294]="V", +[60573]="D", +[60574]="D", +[60575]="D", +[60576]="D", +[60577]="D", +[60578]="D", +[60907]="V", +[60908]="V", +[60909]="V", +[60910]="V", +[60976]="Q", +[60977]="Q", +[60978]="Q", +[61181]="D", +[61182]="D", +[61183]="Q", +[61185]="D", +[61186]="D", +[61187]="D", +[61188]="D", +[61216]="Q", +[61224]="Q", +[61225]="Q", +[61229]="Q", +[61230]="Q", +[61356]="D", +[61357]="Q", +[61358]="Q", +[61359]="Q", +[61360]="D", +[61361]="Q", +[61362]="Q", +[61363]="Q", +[61364]="D", +[61365]="Q", +[61366]="Q", +[61367]="Q", +[61648]="V", +[61649]="V", +[61732]="D", +[61779]="V", +[61780]="V", +[61781]="V", +[61782]="V", +[61783]="D", +[61784]="D", +[61785]="Q", +[61810]="Q", +[61818]="D", +[65000]="V", +[65001]="D", +[65002]="T", +[65003]="D", +[65004]="Q", +[65005]="?", +[65006]="D", +[65007]="Q", +[65008]="V", +[65009]="D", +[65010]="T", +[65011]="T", +[65012]="T", +[65013]="D", +[65014]="D", +[65015]="D", +[65019]="D", +[65021]="V", +[65022]="D", +[65023]="D", +[65024]="D", +[65025]="D", +[65026]="D", +[65027]="D", +[65032]="T", +[65035]="V", +[65036]="V", +[65037]="V", +[65038]="V", +[65039]="V", +[81030]="T", +[81031]="T", +[81032]="T", +[81061]="T", +[81062]="T", +[81063]="T", +[81064]="T", +[81065]="T", +[81066]="T", +[81092]="T", +[81093]="D", +[83280]="T", +[83281]="T", +[83282]="T", +[83283]="T", +[83284]="T", +[83285]="T", +[83286]="T", +[83287]="T", +[83288]="T", +[83289]="T", +[83290]="T", +[83291]="T", +[83292]="T", +[83293]="T", +[83294]="T", +[83295]="T", +[83296]="T", +[83297]="T", +[83309]="Q", +[83400]="T", +[83401]="T", +[83402]="T", +[83403]="T", +[83404]="T", +[83405]="T", +[83410]="T", +[83411]="T", +[83412]="T", +[83413]="T", +[83414]="T", +[83415]="T", +[84040]="Q", +[84041]="V", +["Dalaran Wizard Disguise"]="V", +["Dark Iron Dwarf Disguise"]="V", +["Defias Disguise"]="V", +["Enchant 2H Weapon - Agility"]="V", +["Enchant 2H Weapon - Greater Impact"]="T", +["Enchant 2H Weapon - Impact"]="T", +["Enchant 2H Weapon - Lesser Impact"]="T", +["Enchant 2H Weapon - Lesser Intellect"]="V", +["Enchant 2H Weapon - Lesser Spirit"]="D", +["Enchant 2H Weapon - Major Intellect"]="D", +["Enchant 2H Weapon - Major Spirit"]="D", +["Enchant 2H Weapon - Minor Impact"]="T", +["Enchant 2H Weapon - Minor Intellect"]="D", +["Enchant 2H Weapon - Superior Impact"]="D", +["Enchant Boots - Agility"]="T", +["Enchant Boots - Greater Agility"]="D", +["Enchant Boots - Greater Spirit"]="Q", +["Enchant Boots - Greater Stamina"]="D", +["Enchant Boots - Lesser Agility"]="T", +["Enchant Boots - Lesser Intellect"]="D", +["Enchant Boots - Lesser Spirit"]="D", +["Enchant Boots - Lesser Stamina"]="T", +["Enchant Boots - Major Intellect"]="V", +["Enchant Boots - Minor Agility"]="V", +["Enchant Boots - Minor Speed"]="T", +["Enchant Boots - Minor Stamina"]="T", +["Enchant Boots - Spirit"]="D", +["Enchant Boots - Stamina"]="T", +["Enchant Boots - Superior Stamina"]="D", +["Enchant Boots - Vampirism"]="D", +["Enchant Bracer - Agility"]="D", +["Enchant Bracer - Deflection"]="V", +["Enchant Bracer - Greater Agility"]="V", +["Enchant Bracer - Greater Deflection"]="Q", +["Enchant Bracer - Greater Intellect"]="D", +["Enchant Bracer - Greater Spirit"]="D", +["Enchant Bracer - Greater Stamina"]="D", +["Enchant Bracer - Greater Strength"]="T", +["Enchant Bracer - Healing Power"]="V", +["Enchant Bracer - Intellect"]="T", +["Enchant Bracer - Lesser Deflection"]="V", +["Enchant Bracer - Lesser Intellect"]="T", +["Enchant Bracer - Lesser Spirit"]="D", +["Enchant Bracer - Lesser Stamina"]="T", +["Enchant Bracer - Lesser Strength"]="V", +["Enchant Bracer - Mana Regeneration"]="V", +["Enchant Bracer - Minor Agility"]="T", +["Enchant Bracer - Minor Deflect"]="A", +["Enchant Bracer - Minor Health"]="A", +["Enchant Bracer - Minor Spirit"]="D", +["Enchant Bracer - Minor Stamina"]="T", +["Enchant Bracer - Minor Strength"]="D", +["Enchant Bracer - Spell Power"]="V", +["Enchant Bracer - Spirit"]="T", +["Enchant Bracer - Stamina"]="T", +["Enchant Bracer - Strength"]="T", +["Enchant Bracer - Superior Spirit"]="D", +["Enchant Bracer - Superior Stamina"]="D", +["Enchant Bracer - Superior Strength"]="D", +["Enchant Bracer - Vampirism"]="D", +["Enchant Chest - Greater Health"]="T", +["Enchant Chest - Greater Mana"]="T", +["Enchant Chest - Greater Stats"]="D", +["Enchant Chest - Health"]="T", +["Enchant Chest - Lesser Absorption"]="T", +["Enchant Chest - Lesser Health"]="T", +["Enchant Chest - Lesser Mana"]="V", +["Enchant Chest - Lesser Stats"]="T", +["Enchant Chest - Major Health"]="V", +["Enchant Chest - Major Mana"]="D", +["Enchant Chest - Mana"]="T", +["Enchant Chest - Mighty Mana"]="D", +["Enchant Chest - Minor Absorption"]="T", +["Enchant Chest - Minor Health"]="T", +["Enchant Chest - Minor Mana"]="D", +["Enchant Chest - Minor Stats"]="T", +["Enchant Chest - Stats"]="T", +["Enchant Chest - Superior Health"]="T", +["Enchant Chest - Superior Mana"]="T", +["Enchant Cloak - Defense"]="T", +["Enchant Cloak - Dodge"]="D", +["Enchant Cloak - Fire Resistance"]="T", +["Enchant Cloak - Greater Arcane Resistance"]="D", +["Enchant Cloak - Greater Defense"]="T", +["Enchant Cloak - Greater Fire Resistance"]="V", +["Enchant Cloak - Greater Nature Resistance"]="V", +["Enchant Cloak - Greater Resistance"]="D", +["Enchant Cloak - Lesser Agility"]="D", +["Enchant Cloak - Lesser Fire Resistance"]="T", +["Enchant Cloak - Lesser Protection"]="T", +["Enchant Cloak - Lesser Shadow Resistance"]="D", +["Enchant Cloak - Minor Agility"]="D", +["Enchant Cloak - Minor Protection"]="T", +["Enchant Cloak - Minor Resistance"]="T", +["Enchant Cloak - Resistance"]="T", +["Enchant Cloak - Stealth"]="D", +["Enchant Cloak - Subtlety"]="D", +["Enchant Cloak - Superior Defense"]="V", +["Enchant Gloves - Advanced Herbalism"]="D", +["Enchant Gloves - Advanced Mining"]="D", +["Enchant Gloves - Agility"]="T", +["Enchant Gloves - Arcane Power"]="D", +["Enchant Gloves - Fire Power"]="D", +["Enchant Gloves - Fishing"]="D", +["Enchant Gloves - Frost Power"]="D", +["Enchant Gloves - Greater Agility"]="D", +["Enchant Gloves - Greater Strength"]="D", +["Enchant Gloves - Healing Power"]="D", +["Enchant Gloves - Herbalism"]="D", +["Enchant Gloves - Major Strength"]="Q", +["Enchant Gloves - Mining"]="D", +["Enchant Gloves - Minor Haste"]="T", +["Enchant Gloves - Nature Power"]="D", +["Enchant Gloves - Riding Skill"]="D", +["Enchant Gloves - Shadow Power"]="D", +["Enchant Gloves - Skinning"]="D", +["Enchant Gloves - Strength"]="T", +["Enchant Gloves - Superior Agility"]="D", +["Enchant Gloves - Threat"]="D", +["Enchant Shield - Frost Resistance"]="D", +["Enchant Shield - Greater Spirit"]="T", +["Enchant Shield - Greater Stamina"]="V", +["Enchant Shield - Lesser Block"]="D", +["Enchant Shield - Lesser Protection"]="D", +["Enchant Shield - Lesser Spirit"]="T", +["Enchant Shield - Lesser Stamina"]="T", +["Enchant Shield - Minor Stamina"]="T", +["Enchant Shield - Spirit"]="T", +["Enchant Shield - Stamina"]="D", +["Enchant Shield - Superior Spirit"]="D", +["Enchant Weapon - Agility"]="V", +["Enchant Weapon - Crusader"]="D", +["Enchant Weapon - Demonslaying"]="D", +["Enchant Weapon - Fiery Weapon"]="D", +["Enchant Weapon - Greater Striking"]="T", +["Enchant Weapon - Healing Power"]="D", +["Enchant Weapon - Icy Chill"]="D", +["Enchant Weapon - Lesser Beastslayer"]="D", +["Enchant Weapon - Lesser Elemental Slayer"]="D", +["Enchant Weapon - Lesser Striking"]="T", +["Enchant Weapon - Lifestealing"]="D", +["Enchant Weapon - Mighty Intellect"]="V", +["Enchant Weapon - Mighty Spirit"]="V", +["Enchant Weapon - Minor Beastslayer"]="D", +["Enchant Weapon - Minor Striking"]="T", +["Enchant Weapon - Spell Power"]="D", +["Enchant Weapon - Strength"]="V", +["Enchant Weapon - Striking"]="T", +["Enchant Weapon - Superior Striking"]="D", +["Enchant Weapon - Unholy Weapon"]="D", +["Enchant Weapon - Winter's Might"]="Q", +["Peasant Disguise"]="A", +["Peon Disguise"]="A", +["South Seas Pirate Disguise"]="V", +["Stonesplinter Trogg Disguise"]="V", +["Syndicate Disguise"]="V", +} + +-- Reagents: craftItemID -> { {itemID, count}, ... } +NanamiTradeSkillReagents = { +[65]={{2321,2},{6371,1},{7287,5}}, +[66]={{3486,2},{3577,8},{7071,1}}, +[67]={{7071,1},{7966,1},{8165,14},{12359,4}}, +[82]={{7071,1},{7078,1},{11371,2},{12644,2}}, +[87]={{7071,1},{12644,1},{12655,2},{61673,2}}, +[103]={{7071,1},{7076,1},{7082,1},{12644,2},{22203,2}}, +[118]={{765,1},{2447,1},{3371,1}}, +[131]={{3859,8},{7071,1},{7966,1}}, +[151]={{6037,8},{7071,1},{7966,2}}, +[156]={{3860,12},{55249,3},{6371,3},{3466,2},{55152,2}}, +[724]={{723,1},{2678,1}}, +[733]={{729,1},{730,1},{731,1}}, +[787]={{6303,1}}, +[858]={{118,1},{2450,1}}, +[929]={{2450,1},{2453,1},{3372,1}}, +[1017]={{1015,2},{2665,1}}, +[1082]={{1080,1},{1081,1}}, +[1251]={{2589,1}}, +[1710]={{3356,1},{3357,1},{3372,1}}, +[2300]={{2318,8},{2320,4}}, +[2302]={{2318,2},{2320,1}}, +[2303]={{2318,4},{2320,1}}, +[2304]={{2318,1}}, +[2307]={{2318,7},{2320,2}}, +[2308]={{2318,10},{2321,2}}, +[2309]={{2318,8},{2320,5}}, +[2310]={{2318,5},{2320,2}}, +[2311]={{2318,8},{2320,2},{2324,1}}, +[2312]={{2318,4},{2320,2},{4231,1}}, +[2313]={{2319,4},{2320,1}}, +[2314]={{2319,10},{2321,2},{4231,2}}, +[2315]={{2319,4},{2321,2},{4340,1}}, +[2316]={{2319,8},{2321,1},{4340,1}}, +[2317]={{2319,6},{2321,1},{4340,1}}, +[2318]={{2934,3}}, +[2319]={{2318,4}}, +[2454]={{765,1},{2449,1},{3371,1}}, +[2455]={{765,1},{785,1},{3371,1}}, +[2456]={{785,2},{2447,1},{3371,1}}, +[2457]={{765,1},{2452,1},{3371,1}}, +[2458]={{2447,1},{2449,2},{3371,1}}, +[2459]={{2450,1},{2452,1},{3371,1}}, +[2568]={{2320,1},{2996,1}}, +[2569]={{2318,1},{2320,1},{2996,3}}, +[2570]={{2320,1},{2996,1}}, +[2572]={{2320,2},{2604,2},{2996,3}}, +[2575]={{2320,1},{2604,1},{2996,2}}, +[2576]={{2320,1},{2324,1},{2996,1}}, +[2577]={{2320,1},{2996,2},{6260,1}}, +[2578]={{2318,1},{2321,1},{2996,4}}, +[2579]={{2321,1},{2605,1},{2996,3}}, +[2580]={{2320,3},{2996,2}}, +[2581]={{2589,2}}, +[2582]={{2321,2},{2605,1},{2997,2}}, +[2583]={{2318,2},{2321,2},{2997,4}}, +[2584]={{2321,1},{2997,1}}, +[2585]={{2321,3},{2997,4},{4340,1}}, +[2587]={{2321,1},{2997,2},{4340,1}}, +[2679]={{2672,1}}, +[2680]={{2672,1},{2678,1}}, +[2681]={{769,1}}, +[2682]={{2675,1},{2678,1}}, +[2683]={{2674,1},{2678,1}}, +[2684]={{2673,1}}, +[2685]={{2677,2},{2692,1}}, +[2687]={{2677,1},{2678,1}}, +[2840]={{2770,1}}, +[2841]={{2840,1},{3576,1}}, +[2842]={{2775,1}}, +[2844]={{2589,2},{2840,6},{2880,1}}, +[2845]={{2589,2},{2840,6},{2880,1}}, +[2847]={{2589,2},{2840,6},{2880,1}}, +[2848]={{2319,1},{2841,6},{2880,4}}, +[2849]={{2319,1},{2841,7},{2880,4}}, +[2850]={{2319,2},{2841,5},{2880,4}}, +[2851]={{2840,6}}, +[2852]={{2840,4}}, +[2853]={{2840,2}}, +[2854]={{2840,10},{3470,3}}, +[2857]={{2840,10}}, +[2862]={{2835,1}}, +[2863]={{2836,1}}, +[2864]={{1210,1},{2840,12},{3470,2}}, +[2865]={{2841,6}}, +[2866]={{2841,7}}, +[2867]={{2841,4}}, +[2868]={{2841,5},{3478,2}}, +[2869]={{1705,1},{2841,10},{2842,2},{3478,2}}, +[2870]={{1206,2},{1705,2},{2841,20},{2842,4},{5500,2}}, +[2871]={{2838,1}}, +[2888]={{2886,1},{2894,1}}, +[2892]={{3372,1},{5173,1}}, +[2893]={{3372,1},{5173,2}}, +[2996]={{2589,2}}, +[2997]={{2592,3}}, +[3220]={{3172,1},{3173,1},{3174,1}}, +[3239]={{2589,1},{2835,1}}, +[3240]={{2592,1},{2836,1}}, +[3241]={{2592,1},{2838,1}}, +[3382]={{2447,1},{2449,2},{3371,1}}, +[3383]={{785,1},{2450,2},{3371,1}}, +[3384]={{785,3},{3355,1},{3371,1}}, +[3385]={{785,1},{3371,1},{3820,1}}, +[3386]={{1288,1},{2453,1},{3372,1}}, +[3387]={{8839,2},{8845,1},{8925,1}}, +[3388]={{2450,2},{2453,2},{3372,1}}, +[3389]={{3355,1},{3372,1},{3820,1}}, +[3390]={{2452,1},{3355,1},{3372,1}}, +[3391]={{2449,1},{3356,1},{3372,1}}, +[3469]={{2840,8}}, +[3470]={{2835,2}}, +[3471]={{774,1},{2840,8},{3470,2}}, +[3472]={{2840,8},{3470,2}}, +[3473]={{2321,2},{2840,8},{3470,3}}, +[3474]={{774,1},{818,1},{2840,8}}, +[3478]={{2836,2}}, +[3480]={{1210,1},{2841,5},{3478,1}}, +[3481]={{2841,8},{2842,2},{3478,2}}, +[3482]={{2841,6},{2842,1},{3478,2}}, +[3483]={{2841,8},{2842,1},{3478,2}}, +[3484]={{1705,2},{2605,1},{3478,2},{3575,4}}, +[3485]={{2605,1},{3478,2},{3575,4},{5498,2}}, +[3486]={{2838,3}}, +[3487]={{818,2},{2319,2},{2840,14},{2880,2}}, +[3488]={{774,2},{2318,2},{2840,12},{2880,2},{3470,2}}, +[3489]={{2318,2},{2840,10},{2842,2},{2880,2},{3470,2}}, +[3490]={{1210,2},{2319,2},{2459,1},{2841,4},{3466,1},{3478,2}}, +[3491]={{1206,1},{1210,1},{2319,2},{2841,8},{3466,1},{3478,2}}, +[3492]={{1705,2},{2319,2},{3391,1},{3466,2},{3478,2},{3575,6}}, +[3530]={{2592,1}}, +[3531]={{2592,2}}, +[3575]={{2772,1}}, +[3576]={{2771,1}}, +[3577]={{3575,1}}, +[3662]={{2678,1},{2924,1}}, +[3663]={{1468,2},{2692,1}}, +[3664]={{2692,1},{3667,1}}, +[3665]={{2692,1},{3685,1}}, +[3666]={{2251,2},{2692,1}}, +[3719]={{2321,2},{4234,5}}, +[3726]={{2692,1},{3730,1}}, +[3727]={{2692,1},{3731,1}}, +[3728]={{3713,1},{3731,2}}, +[3729]={{3712,1},{3713,1}}, +[3775]={{2930,1},{3371,1}}, +[3776]={{8923,3},{8925,1}}, +[3823]={{3355,1},{3372,1},{3818,1}}, +[3824]={{3369,4},{3372,1},{3818,4}}, +[3825]={{3355,1},{3372,1},{3821,1}}, +[3826]={{2453,1},{3357,1},{3372,1}}, +[3827]={{3356,1},{3372,1},{3820,1}}, +[3828]={{3358,1},{3372,1},{3818,1}}, +[3829]={{3358,4},{3372,1},{3819,2}}, +[3835]={{2605,1},{3575,6}}, +[3836]={{2605,1},{3575,12},{3864,1}}, +[3837]={{3486,2},{3577,2},{3859,8}}, +[3840]={{2605,1},{3486,1},{3575,7}}, +[3841]={{3486,1},{3577,2},{3859,6}}, +[3842]={{2605,1},{3486,1},{3575,8}}, +[3843]={{3486,1},{3575,10},{3577,2}}, +[3844]={{1206,2},{1529,2},{3486,4},{3575,20},{4255,1}}, +[3845]={{1529,2},{3486,4},{3577,2},{3859,12}}, +[3846]={{1705,1},{3486,2},{3859,8},{3864,1}}, +[3847]={{3486,4},{3577,4},{3859,10},{3864,1}}, +[3848]={{818,1},{2319,1},{2841,6},{2880,4},{3470,2}}, +[3849]={{1705,2},{3466,2},{3486,1},{3575,6},{4234,3}}, +[3850]={{1529,2},{3466,2},{3486,2},{3575,8},{4234,3}}, +[3851]={{2842,4},{3466,2},{3486,1},{3575,8},{4234,2}}, +[3852]={{1705,2},{3466,2},{3486,2},{3575,10},{3577,4},{4234,2}}, +[3853]={{1705,3},{3466,2},{3486,2},{3859,8},{4234,3}}, +[3854]={{1529,2},{3466,2},{3486,2},{3829,1},{3859,8},{4234,4}}, +[3855]={{3466,2},{3486,2},{3575,14},{3577,4},{4234,2}}, +[3856]={{3466,2},{3486,3},{3824,1},{3859,10},{3864,2},{4234,3}}, +[3859]={{3575,1},{3857,1}}, +[3860]={{3858,1}}, +[3928]={{3358,1},{8838,1},{8925,1}}, +[4231]={{783,1},{4289,1}}, +[4233]={{4232,1},{4289,1}}, +[4234]={{2319,5}}, +[4236]={{4235,1},{4289,3}}, +[4237]={{2318,6},{2320,1}}, +[4238]={{2320,3},{2996,3}}, +[4239]={{2318,3},{2320,2}}, +[4240]={{2321,1},{2997,3}}, +[4241]={{2321,1},{2605,1},{2997,4}}, +[4242]={{2318,6},{2320,2},{4231,1}}, +[4243]={{2318,6},{2320,4},{4231,3}}, +[4244]={{2320,2},{4231,2},{4243,1}}, +[4245]={{2321,3},{4234,2},{4305,3}}, +[4246]={{2318,6},{2320,2}}, +[4247]={{2319,14},{2321,4}}, +[4248]={{2312,1},{2321,1},{4233,1},{4340,1}}, +[4249]={{2321,2},{4233,1},{4246,1},{4340,1}}, +[4250]={{2319,8},{2321,2},{3383,1}}, +[4251]={{2319,4},{2321,1},{4233,1}}, +[4252]={{2319,12},{2321,2},{3390,1},{4340,1}}, +[4253]={{2319,4},{2321,2},{3182,2},{3389,2},{4233,2}}, +[4254]={{2321,1},{4234,6},{5637,2}}, +[4255]={{2321,4},{2605,2},{4234,9}}, +[4256]={{2321,2},{3824,1},{4234,12},{4236,2}}, +[4257]={{2321,1},{2605,1},{4234,5},{4236,1},{7071,1}}, +[4258]={{2321,1},{4234,4},{4236,2},{7071,1}}, +[4259]={{2321,1},{2605,1},{4234,6},{4236,2}}, +[4260]={{4234,6},{4236,2},{4291,1}}, +[4262]={{1529,2},{2321,1},{3864,1},{4236,4},{5500,2}}, +[4264]={{4096,2},{4234,6},{4236,2},{4291,1},{5633,1},{7071,1}}, +[4265]={{2321,1},{4234,5}}, +[4304]={{4234,6}}, +[4305]={{4306,4}}, +[4307]={{2320,1},{2996,2}}, +[4308]={{2320,2},{2605,1},{2996,3}}, +[4309]={{2321,2},{2996,4}}, +[4310]={{2321,1},{2997,3}}, +[4311]={{2321,2},{2997,3},{5498,2}}, +[4312]={{2318,2},{2321,1},{2996,5}}, +[4313]={{2318,2},{2321,1},{2604,2},{2997,4}}, +[4314]={{2321,2},{2997,3}}, +[4315]={{2319,2},{2321,2},{2997,6}}, +[4316]={{2321,4},{2997,5}}, +[4317]={{2321,3},{2997,6},{5500,1}}, +[4318]={{2321,3},{2997,4},{3383,1}}, +[4319]={{2321,2},{4234,2},{4305,3},{6260,2}}, +[4320]={{2319,4},{3182,4},{4305,2},{5500,2}}, +[4321]={{2321,2},{3182,1},{4305,3}}, +[4322]={{2321,2},{4305,3},{4337,2}}, +[4323]={{3824,1},{4291,1},{4305,4}}, +[4324]={{4305,5},{6260,4}}, +[4325]={{4291,1},{4305,4},{4337,2}}, +[4326]={{3827,1},{4291,1},{4305,4}}, +[4327]={{3829,1},{4291,2},{4337,2},{4339,3}}, +[4328]={{4305,4},{4337,2},{7071,1}}, +[4329]={{3864,1},{4234,4},{4291,1},{4339,4},{7071,1}}, +[4330]={{2321,1},{2604,2},{2997,3}}, +[4331]={{2321,4},{2324,2},{2997,4},{5500,1}}, +[4332]={{2321,1},{4305,1},{4341,1}}, +[4333]={{2321,1},{4305,2},{4340,2}}, +[4334]={{2321,1},{2324,2},{4305,3}}, +[4335]={{4291,1},{4305,4},{4342,1}}, +[4336]={{2325,1},{4291,1},{4305,5}}, +[4339]={{4338,5}}, +[4343]={{2320,1},{2996,2}}, +[4344]={{2320,1},{2996,1}}, +[4357]={{2835,1}}, +[4358]={{2589,1},{4357,2}}, +[4359]={{2840,1}}, +[4360]={{2589,1},{2840,1},{4357,2},{4359,1}}, +[4361]={{2840,2},{2880,1}}, +[4362]={{4359,1},{4361,1},{4399,1}}, +[4363]={{2589,2},{2840,1},{4359,2}}, +[4364]={{2836,1}}, +[4365]={{2589,1},{4364,3}}, +[4366]={{2592,1},{2841,1},{4359,2},{4363,1}}, +[4367]={{159,1},{2318,1},{4363,1},{4364,2}}, +[4368]={{818,2},{2318,6}}, +[4369]={{2319,2},{4359,4},{4361,2},{4399,1}}, +[4370]={{2840,3},{4364,4},{4404,1}}, +[4371]={{2841,2},{2880,1}}, +[4372]={{1206,3},{4359,2},{4371,2},{4400,1}}, +[4373]={{1210,2},{2319,4}}, +[4374]={{2592,1},{2841,2},{4364,4},{4404,1}}, +[4375]={{2592,1},{2841,2}}, +[4376]={{4375,1},{4402,1}}, +[4377]={{2838,1}}, +[4378]={{2592,1},{4377,2}}, +[4379]={{2842,3},{4371,2},{4375,2},{4400,1}}, +[4380]={{2841,3},{4377,2},{4404,1}}, +[4381]={{1206,1},{2319,2},{4371,1},{4375,2}}, +[4382]={{2319,1},{2592,1},{2841,2}}, +[4383]={{1705,2},{4371,3},{4375,3},{4400,1}}, +[4384]={{2592,2},{4375,1},{4377,2},{4382,1}}, +[4385]={{1206,2},{2319,4},{4368,1}}, +[4386]={{3829,1},{4375,1}}, +[4387]={{3575,2}}, +[4388]={{1529,1},{4306,2},{4371,1},{4375,3}}, +[4389]={{3575,1},{10558,1}}, +[4390]={{3575,1},{4306,1},{4377,1}}, +[4391]={{4234,4},{4382,1},{4387,2},{4389,2}}, +[4392]={{4234,4},{4382,1},{4387,1},{4389,1}}, +[4393]={{3864,2},{4234,6}}, +[4394]={{3575,3},{4377,3},{4404,1}}, +[4395]={{3575,2},{4377,3},{4389,1}}, +[4396]={{3864,2},{4382,1},{4387,4},{4389,4},{7191,1}}, +[4397]={{1529,2},{1705,2},{3864,2},{4389,4},{7191,1}}, +[4398]={{159,1},{4234,2},{10505,2}}, +[4401]={{774,2},{2840,1},{4359,1},{4363,1}}, +[4403]={{2319,4},{4371,4},{4377,4},{4387,1}}, +[4404]={{2842,1}}, +[4405]={{774,1},{4359,1},{4361,1}}, +[4406]={{1206,1},{4371,1}}, +[4407]={{1529,1},{3864,1},{4371,1}}, +[4455]={{2321,2},{4234,4},{4461,6}}, +[4456]={{2321,2},{4234,4},{4461,4}}, +[4457]={{2692,1},{3404,1}}, +[4592]={{6289,1}}, +[4593]={{6308,1}}, +[4594]={{6362,1}}, +[4596]={{2447,1},{3164,1},{3371,1}}, +[4623]={{3372,1},{3821,1},{3858,1}}, +[4852]={{4306,1},{4377,1},{4611,1}}, +[5081]={{2318,4},{2320,1},{5082,3}}, +[5095]={{6361,1}}, +[5237]={{2928,1},{2930,1},{3371,1}}, +[5472]={{5465,1}}, +[5473]={{5466,1}}, +[5474]={{2678,1},{5467,1}}, +[5476]={{2678,1},{5468,1}}, +[5477]={{4536,1},{5469,1}}, +[5478]={{5051,1}}, +[5479]={{2692,1},{5470,1}}, +[5480]={{2678,4},{5471,1}}, +[5507]={{1206,1},{4363,1},{4371,2},{4375,2}}, +[5525]={{159,1},{5503,1}}, +[5526]={{1179,1},{2678,1},{5503,1}}, +[5527]={{2692,1},{5504,1}}, +[5530]={{3818,1}}, +[5540]={{2841,6},{3466,1},{3478,2},{5498,2}}, +[5541]={{2319,2},{2841,10},{3466,1},{3478,2},{5500,1}}, +[5542]={{2321,2},{2997,3},{5498,1}}, +[5631]={{2450,1},{3371,1},{5635,1}}, +[5633]={{3356,1},{3372,1},{5637,1}}, +[5634]={{3372,1},{3820,1},{6370,2}}, +[5739]={{2321,2},{4234,14},{7071,1}}, +[5762]={{2321,1},{2604,1},{2996,4}}, +[5763]={{2321,1},{2604,1},{2997,4}}, +[5764]={{2321,3},{2605,1},{4234,3},{4305,4}}, +[5765]={{2321,4},{2325,1},{4305,5}}, +[5766]={{2321,2},{3182,2},{4305,2}}, +[5770]={{2321,2},{3182,2},{4305,4}}, +[5780]={{2318,6},{2321,1},{5784,8}}, +[5781]={{2318,8},{2321,1},{4231,1},{5784,12}}, +[5782]={{2321,3},{4234,10},{4236,1},{5785,12}}, +[5783]={{4234,14},{4236,1},{4291,1},{5785,16}}, +[5957]={{2318,3},{2320,1}}, +[5958]={{2319,8},{2321,1},{2997,1}}, +[5961]={{2319,12},{2321,1},{4340,1}}, +[5962]={{2321,2},{4234,12},{4305,2}}, +[5963]={{1206,1},{2321,2},{4234,10}}, +[5964]={{2321,2},{4234,8},{4236,1}}, +[5965]={{4234,14},{4291,2},{4305,2}}, +[5966]={{4234,4},{4236,1},{4291,1}}, +[5996]={{3371,1},{3820,1},{6370,2}}, +[5997]={{765,2},{3371,1}}, +[6037]={{3860,1}}, +[6038]={{2692,1},{4655,1}}, +[6040]={{3486,2},{3859,5}}, +[6041]={{3486,2},{3859,8},{4234,4}}, +[6042]={{3478,4},{3575,6}}, +[6043]={{1705,1},{3478,2},{3575,4}}, +[6048]={{3356,1},{3369,1},{3372,1}}, +[6049]={{3372,1},{4402,1},{6371,1}}, +[6050]={{3372,1},{3819,1},{3821,1}}, +[6051]={{2452,1},{2453,1},{3371,1}}, +[6052]={{3357,1},{3372,1},{3820,1}}, +[6149]={{3358,1},{3372,1},{3821,1}}, +[6214]={{2318,2},{2840,12},{2880,2}}, +[6218]={{6217,1},{10938,1},{10940,1}}, +[6219]={{2840,6}}, +[6238]={{2320,1},{2996,3}}, +[6239]={{2320,1},{2604,1},{2996,3}}, +[6240]={{2320,1},{2996,3},{6260,1}}, +[6241]={{2320,1},{2324,1},{2996,3}}, +[6242]={{2320,2},{2996,4},{6260,2}}, +[6243]={{2321,2},{2605,1},{2997,3}}, +[6263]={{2321,2},{2997,4},{6260,2}}, +[6264]={{2321,3},{2604,3},{2997,5}}, +[6290]={{6291,1}}, +[6316]={{2678,1},{6317,1}}, +[6338]={{2842,1},{3470,2}}, +[6339]={{1210,1},{6338,1},{10939,3},{10940,6}}, +[6350]={{2841,6},{3470,6}}, +[6370]={{3371,1},{6358,2}}, +[6371]={{3371,1},{6359,2}}, +[6372]={{2452,1},{3371,1},{6370,1}}, +[6373]={{3356,1},{3372,1},{6371,2}}, +[6384]={{2321,1},{2997,4},{4340,1},{6260,2}}, +[6385]={{2321,1},{2605,2},{2997,4},{4340,1}}, +[6450]={{4306,1}}, +[6451]={{4306,2}}, +[6452]={{1475,1}}, +[6453]={{1288,1}}, +[6466]={{2321,1},{4231,1},{6470,8}}, +[6467]={{2321,2},{6470,6},{6471,2}}, +[6468]={{2321,2},{6470,10},{6471,10}}, +[6533]={{2841,2},{4364,1},{6530,1}}, +[6657]={{2678,1},{6522,1}}, +[6662]={{2449,1},{3371,1},{6522,1}}, +[6709]={{2318,6},{2320,4},{4231,1},{5498,1}}, +[6712]={{2841,1},{2880,1},{4359,2}}, +[6714]={{2592,1},{4364,4}}, +[6730]={{774,2},{2840,12},{3470,2}}, +[6731]={{818,2},{2840,16},{3470,3}}, +[6733]={{1210,3},{2841,8},{3478,4}}, +[6786]={{2320,1},{2324,1},{2996,2},{6260,1}}, +[6787]={{2321,1},{2324,4},{2997,3}}, +[6795]={{2324,2},{4291,1},{4305,3}}, +[6796]={{2604,2},{4291,1},{4305,3}}, +[6887]={{4603,1}}, +[6888]={{2678,1},{6889,1}}, +[6890]={{3173,1}}, +[6947]={{2928,1},{3371,1}}, +[6949]={{2928,3},{3372,1}}, +[6950]={{3372,1},{8924,1}}, +[6951]={{2928,4},{2930,4},{3372,1}}, +[7026]={{2320,1},{2996,1}}, +[7027]={{2319,2},{2321,2},{4305,3},{6048,1}}, +[7046]={{2321,3},{4305,4},{6260,2}}, +[7047]={{2321,2},{4234,2},{4305,3},{6048,2}}, +[7048]={{2321,1},{4305,2},{6260,2}}, +[7049]={{929,4},{2321,1},{4234,2},{4305,3}}, +[7050]={{2321,2},{4305,3}}, +[7051]={{2321,2},{4305,3},{7067,1}}, +[7052]={{2321,2},{4305,4},{6260,2},{7070,1},{7071,1}}, +[7053]={{2321,2},{4305,3},{6260,2}}, +[7054]={{4291,2},{4339,2},{7067,2},{7068,2},{7069,2},{7070,2}}, +[7055]={{2604,2},{4291,1},{4305,4},{7071,1}}, +[7056]={{2604,2},{4291,1},{4305,5},{6371,2}}, +[7057]={{4291,2},{4305,5}}, +[7058]={{2321,2},{2604,2},{4305,4}}, +[7059]={{2604,2},{4291,2},{4305,5},{6371,2}}, +[7060]={{4291,2},{4305,6},{6260,2},{7072,2}}, +[7061]={{4234,4},{4291,2},{4305,5},{7067,4},{7071,1}}, +[7062]={{2604,2},{4291,2},{4305,4}}, +[7063]={{2604,4},{3827,2},{4291,1},{4305,8},{7068,4}}, +[7064]={{2604,4},{4291,2},{4304,2},{4305,6},{6371,2},{7068,2}}, +[7065]={{2605,2},{4291,1},{4305,5}}, +[7067]={{7075,1}}, +[7068]={{7077,1}}, +[7070]={{7079,1}}, +[7071]={{3575,1}}, +[7076]={{7078,1}}, +[7078]={{7082,1}}, +[7080]={{7076,1}}, +[7082]={{7080,1}}, +[7148]={{814,2},{1210,2},{3575,6},{4306,2},{4375,2},{7191,1}}, +[7166]={{2318,1},{2840,6},{2880,1},{3470,1}}, +[7189]={{4234,4},{9061,2},{10026,1},{10559,2},{10560,1}}, +[7276]={{2318,2},{2320,1}}, +[7277]={{2318,2},{2320,3}}, +[7278]={{2318,4},{2320,2}}, +[7279]={{2318,3},{2320,4}}, +[7280]={{2318,5},{2320,5}}, +[7281]={{2318,6},{2320,4}}, +[7282]={{2318,10},{2321,1},{4231,1}}, +[7283]={{2319,4},{2321,1},{7286,12}}, +[7284]={{2319,4},{2321,1},{7287,6}}, +[7285]={{2319,6},{2321,1},{2457,1}}, +[7348]={{2319,8},{2321,2},{5116,4}}, +[7349]={{2319,8},{2321,2},{3356,4}}, +[7352]={{2319,6},{2321,2},{7067,1}}, +[7358]={{2319,10},{2321,2},{5373,2}}, +[7359]={{2319,12},{2321,2},{2997,2},{7067,2}}, +[7371]={{2321,2},{4234,8}}, +[7372]={{2321,2},{4234,8}}, +[7373]={{2321,2},{2325,1},{4234,10}}, +[7374]={{2321,2},{3824,1},{4234,10}}, +[7375]={{2321,2},{4234,10},{7392,4}}, +[7377]={{2321,2},{4234,6},{7067,2},{7070,2}}, +[7378]={{2325,1},{4234,16},{4291,2}}, +[7386]={{4234,8},{4291,2},{7392,6}}, +[7387]={{2325,2},{4234,10},{4305,2},{7071,1}}, +[7390]={{3824,1},{4234,8},{4291,2},{7428,2}}, +[7391]={{2459,2},{4234,10},{4291,1},{4337,2}}, +[7506]={{774,1},{814,2},{818,1},{2841,6},{4375,1}}, +[7676]={{159,1},{2452,1}}, +[7913]={{1210,2},{3486,2},{3575,8},{5635,4}}, +[7914]={{3486,4},{3575,20}}, +[7915]={{3575,10},{5635,2},{5637,2}}, +[7916]={{818,4},{3486,2},{3575,12},{5637,4}}, +[7917]={{3486,3},{3575,14},{5637,2}}, +[7918]={{3860,8},{4234,6}}, +[7919]={{3860,6},{4338,4}}, +[7920]={{3860,12}}, +[7921]={{1705,2},{3860,10}}, +[7922]={{3859,14},{7966,1}}, +[7924]={{3860,8},{3864,2}}, +[7925]={{3860,8},{4234,6},{4338,4}}, +[7926]={{3860,12},{6037,1},{7909,1},{7966,1}}, +[7927]={{3860,10},{4338,6},{6037,1},{7966,1}}, +[7928]={{3860,12},{4304,6},{6037,1}}, +[7929]={{3860,12},{7067,1}}, +[7930]={{3860,16}}, +[7931]={{3860,10},{4338,6}}, +[7932]={{3860,14},{3864,4},{4304,4}}, +[7933]={{3860,14},{4304,4}}, +[7934]={{3860,14},{7909,1}}, +[7935]={{3860,16},{6037,6},{7077,1},{7966,1}}, +[7936]={{3860,14},{4304,4},{6037,2},{7909,1},{7966,1}}, +[7937]={{3860,16},{6037,2},{7966,1},{7971,1}}, +[7938]={{3860,10},{3864,3},{5966,1},{6037,8},{7909,3},{7966,2}}, +[7939]={{3860,12},{6037,24},{7910,4},{7966,2},{7971,4}}, +[7941]={{3860,12},{3864,2},{4234,4},{7966,1}}, +[7942]={{3860,16},{4304,4},{7909,2},{7966,1}}, +[7943]={{3860,14},{4304,2},{6037,4},{7966,1}}, +[7944]={{1206,2},{1705,2},{3860,14},{4338,2},{7909,1},{7966,1}}, +[7945]={{1210,4},{3860,16},{4304,2},{7966,1},{7971,1}}, +[7946]={{3860,18},{4304,4},{7075,2},{7966,1}}, +[7947]={{3860,12},{4304,2},{6037,6},{7910,2},{7966,1}}, +[7954]={{1529,5},{3860,24},{3864,5},{4304,4},{6037,6},{7075,4},{7966,4}}, +[7955]={{2318,1},{2840,10},{2880,2},{3470,1}}, +[7956]={{2319,1},{2841,8},{3466,1}}, +[7957]={{2319,2},{2841,12},{3466,2}}, +[7958]={{2319,2},{2841,14},{3466,1}}, +[7959]={{3860,28},{4304,6},{6037,10},{7966,6},{7972,10}}, +[7960]={{3860,30},{4304,6},{6037,16},{7081,4},{7910,6},{7966,8}}, +[7961]={{3823,2},{3860,28},{4304,2},{6037,8},{7081,6},{7909,6},{7966,4}}, +[7963]={{3486,3},{3859,16}}, +[7964]={{7912,1}}, +[7965]={{4306,1},{7912,1}}, +[7966]={{7912,4}}, +[7967]={{3860,4},{6037,2},{7966,4}}, +[7969]={{3860,4},{7966,3}}, +[8067]={{2840,1},{4357,1}}, +[8068]={{2840,1},{4364,1}}, +[8069]={{2841,1},{4377,1}}, +[8170]={{4304,6}}, +[8172]={{8150,1},{8169,1}}, +[8173]={{4291,1},{4304,5}}, +[8174]={{4234,12},{4236,2},{4291,2}}, +[8175]={{4291,2},{4304,7}}, +[8176]={{4291,2},{4304,5}}, +[8185]={{4304,14},{8167,28},{8343,1}}, +[8187]={{4304,6},{8167,8},{8343,1}}, +[8189]={{4304,6},{8167,12},{8343,1}}, +[8191]={{4304,14},{8167,24},{8343,1}}, +[8192]={{4291,3},{4304,8},{4338,6}}, +[8193]={{4291,4},{4304,14}}, +[8195]={{4291,4},{4304,12}}, +[8197]={{4304,16},{8343,2}}, +[8198]={{4304,8},{8167,12},{8343,1}}, +[8200]={{4304,10},{8151,4},{8343,1}}, +[8201]={{4304,8},{8151,6},{8343,1}}, +[8202]={{4304,10},{8152,6},{8343,2}}, +[8203]={{4291,4},{4304,12},{8154,12}}, +[8204]={{4291,2},{4304,6},{8154,8}}, +[8205]={{4291,2},{4304,10},{8154,4}}, +[8206]={{4304,14},{8154,8},{8343,2}}, +[8207]={{4304,12},{8154,16},{8343,2}}, +[8208]={{4304,10},{8154,20},{8343,2}}, +[8209]={{4291,6},{4304,12},{8154,12}}, +[8210]={{4304,10},{8153,1},{8172,1}}, +[8211]={{4304,12},{8153,2},{8172,1}}, +[8212]={{4304,16},{8153,6},{8172,2}}, +[8213]={{4304,14},{8153,4},{8172,2}}, +[8214]={{4304,10},{8153,2},{8172,1}}, +[8215]={{4304,16},{8153,6},{8172,2}}, +[8216]={{4304,14},{8152,4},{8343,2}}, +[8217]={{4291,4},{4304,12},{8172,1},{8949,1}}, +[8218]={{4291,6},{4304,10},{8172,1},{8951,1}}, +[8345]={{4304,18},{8146,8},{8172,2},{8343,4},{8368,2}}, +[8346]={{4304,20},{7075,2},{7079,8},{8172,1},{8343,4}}, +[8347]={{4304,24},{8165,12},{8172,2},{8343,4}}, +[8348]={{4304,40},{7075,4},{7077,8},{8172,2},{8343,4}}, +[8349]={{4304,40},{7971,2},{8168,40},{8172,4},{8343,4}}, +[8364]={{8365,1}}, +[8367]={{4304,40},{8165,30},{8172,4},{8343,4}}, +[8544]={{4338,1}}, +[8545]={{4338,2}}, +[8546]={{7078,2},{8150,4},{18512,1}}, +[8926]={{8924,2},{8925,1}}, +[8927]={{8924,3},{8925,1}}, +[8928]={{8924,4},{8925,1}}, +[8949]={{3372,1},{3820,1},{3821,1}}, +[8951]={{3355,1},{3372,1},{3821,1}}, +[8956]={{3821,1},{4625,1},{8925,1}}, +[8984]={{5173,3},{8925,1}}, +[8985]={{5173,5},{8925,1}}, +[9030]={{3821,1},{7067,1},{8925,1}}, +[9036]={{3358,1},{8831,1},{8925,1}}, +[9060]={{3577,1},{3860,5},{6037,1}}, +[9061]={{3372,1},{4625,1},{9260,1}}, +[9088]={{8836,1},{8839,1},{8925,1}}, +[9144]={{8153,1},{8831,1},{8925,1}}, +[9149]={{3575,4},{4625,4},{8831,4},{9262,1}}, +[9154]={{8836,1},{8925,1}}, +[9155]={{3821,1},{8839,1},{8925,1}}, +[9172]={{8838,1},{8845,1},{8925,1}}, +[9179]={{3358,1},{8839,1},{8925,1}}, +[9186]={{8923,2},{8924,2},{8925,1}}, +[9187]={{3821,1},{8838,1},{8925,1}}, +[9197]={{8831,3},{8925,1}}, +[9206]={{8838,1},{8846,1},{8925,1}}, +[9210]={{4342,1},{8845,2},{8925,1}}, +[9224]={{8845,1},{8846,1},{8925,1}}, +[9233]={{8846,2},{8925,1}}, +[9264]={{8845,3},{8925,1}}, +[9312]={{4234,1},{4377,1}}, +[9313]={{4234,1},{4377,1}}, +[9318]={{4234,1},{4377,1}}, +[9366]={{3486,4},{3577,4},{3859,10},{3864,1}}, +[9998]={{4291,3},{4339,2}}, +[9999]={{4291,3},{4339,2}}, +[10001]={{4339,3},{8343,1}}, +[10002]={{4339,3},{8343,1},{10285,2}}, +[10003]={{4339,2},{8343,2}}, +[10004]={{4339,3},{8343,1},{10285,2}}, +[10007]={{2604,2},{4339,3},{8343,1}}, +[10008]={{2324,1},{4339,1},{8343,1}}, +[10009]={{2604,2},{4339,3},{8343,1}}, +[10010]={{4339,4},{7079,2},{8343,2}}, +[10011]={{4339,3},{7079,2},{8343,2}}, +[10018]={{2604,2},{4339,3},{8343,2}}, +[10019]={{4339,4},{8153,4},{8343,2},{10286,2}}, +[10020]={{4339,5},{7079,3},{8343,2}}, +[10021]={{4339,6},{8153,6},{8343,2},{10286,2}}, +[10023]={{4339,5},{8343,2},{10285,5}}, +[10024]={{4339,3},{8343,2}}, +[10025]={{4339,2},{8343,2},{10285,8}}, +[10026]={{4304,2},{4339,3},{8343,2}}, +[10027]={{4339,3},{8343,2}}, +[10028]={{4339,5},{8343,2},{10285,4}}, +[10029]={{2604,2},{4339,4},{8343,3}}, +[10030]={{4339,3},{4589,6},{8343,2}}, +[10031]={{4304,2},{4339,6},{8343,3},{10285,6}}, +[10032]={{4339,4},{7079,4},{8343,2}}, +[10033]={{2604,2},{4339,4},{8343,2}}, +[10034]={{4339,4},{8343,2}}, +[10035]={{4339,4},{8343,3}}, +[10036]={{4339,5},{8343,3}}, +[10038]={{4339,5},{7079,6},{8343,3}}, +[10039]={{4304,2},{4339,6},{7079,6},{8343,3}}, +[10040]={{2324,1},{4339,5},{8343,3}}, +[10041]={{1529,1},{4339,8},{6037,1},{8153,4},{8343,3},{10286,2}}, +[10042]={{4339,5},{7077,2},{8343,2}}, +[10044]={{4304,2},{4339,5},{7077,1},{8343,3}}, +[10045]={{2320,1},{2996,1}}, +[10046]={{2318,1},{2320,1},{2996,2}}, +[10047]={{2321,1},{2996,4}}, +[10048]={{2321,1},{2604,3},{2997,5}}, +[10050]={{4291,2},{4339,4}}, +[10051]={{2604,2},{4339,4},{8343,2}}, +[10052]={{4339,2},{6261,2},{8343,1}}, +[10053]={{2324,1},{2325,1},{4339,3},{8343,1}}, +[10054]={{4339,2},{4342,2},{8343,2}}, +[10055]={{4339,3},{8343,1},{10290,1}}, +[10056]={{4339,1},{6261,1},{8343,1}}, +[10421]={{2840,4}}, +[10423]={{2841,12},{2842,4},{3478,2}}, +[10498]={{3859,4}}, +[10499]={{3864,2},{4234,6}}, +[10500]={{3864,2},{4234,4},{4385,1},{7068,2}}, +[10501]={{4304,4},{7909,2},{10592,1}}, +[10502]={{4304,4},{7910,2}}, +[10503]={{4304,6},{7910,2}}, +[10504]={{1529,3},{4304,8},{7909,3},{8153,2},{10286,2}}, +[10505]={{7912,2}}, +[10506]={{774,4},{818,4},{3860,8},{6037,1},{10561,1}}, +[10507]={{4306,1},{10505,1}}, +[10508]={{3860,4},{4400,1},{7068,2},{10559,1},{10560,1}}, +[10510]={{3860,6},{3864,2},{4400,1},{10559,2},{10560,1}}, +[10512]={{3860,1},{10505,1}}, +[10513]={{3860,2},{10505,2}}, +[10514]={{10505,1},{10560,1},{10561,1}}, +[10518]={{4339,4},{10285,2},{10505,4},{10560,1}}, +[10542]={{3860,8},{3864,1},{7067,4}}, +[10543]={{3860,8},{3864,1},{7068,4}}, +[10545]={{4234,2},{8151,2},{10500,1},{10558,2},{10559,1}}, +[10546]={{4304,2},{7909,2},{10559,1}}, +[10548]={{6037,2},{7910,1},{10559,1}}, +[10558]={{3577,1}}, +[10559]={{3860,3}}, +[10560]={{3860,1},{4338,1},{10505,1}}, +[10561]={{3860,3}}, +[10562]={{10505,2},{10560,1},{10561,2}}, +[10576]={{3860,14},{6037,4},{7077,4},{7910,2},{9060,2},{9061,2}}, +[10577]={{3860,4},{7068,1},{10505,5},{10558,1},{10559,2}}, +[10585]={{3860,2},{4389,1},{10560,1},{10561,1}}, +[10586]={{9061,1},{10507,6},{10560,1},{10561,1}}, +[10587]={{4407,2},{6037,6},{10505,4},{10560,1},{10561,2}}, +[10588]={{3860,4},{9061,4},{10543,1},{10560,1}}, +[10592]={{3372,1},{3818,1},{3821,1}}, +[10644]={{10647,1},{10648,1}}, +[10645]={{7972,4},{9060,1},{10559,2},{10560,1},{12808,1}}, +[10646]={{4338,1},{10505,3},{10560,1}}, +[10713]={{10647,1},{10648,1}}, +[10716]={{1529,2},{3860,4},{8151,4},{10559,1},{10560,1}}, +[10720]={{3860,4},{4337,4},{10285,2},{10505,2},{10559,1}}, +[10721]={{3860,4},{6037,2},{7387,1},{7909,2},{10560,1}}, +[10724]={{4234,4},{4389,4},{10026,1},{10505,8},{10559,2}}, +[10725]={{1529,2},{3860,6},{6037,6},{9060,2},{10558,1},{10561,1}}, +[10726]={{3860,10},{4338,4},{6037,4},{7910,2},{10558,1}}, +[10727]={{3860,6},{6037,6},{9061,4},{10559,2},{10560,1}}, +[10841]={{159,1},{3821,1}}, +[10918]={{2930,1},{3372,1},{5173,1}}, +[10920]={{2930,1},{3372,1},{5173,2}}, +[10921]={{5173,2},{8923,1},{8925,1}}, +[10922]={{5173,2},{8923,2},{8925,1}}, +[11128]={{3478,2},{3577,1}}, +[11130]={{5500,1},{11082,2},{11083,2},{11128,1}}, +[11144]={{3486,1},{6037,1}}, +[11145]={{7971,1},{11135,2},{11137,2},{11144,1}}, +[11287]={{4470,1},{10938,1}}, +[11288]={{4470,1},{10939,1}}, +[11289]={{11083,1},{11134,1},{11291,1}}, +[11290]={{11135,1},{11137,1},{11291,1}}, +[11371]={{11370,8}}, +[11590]={{3860,1},{4338,1},{10505,1}}, +[11604]={{7077,8},{11371,20}}, +[11605]={{7077,1},{11371,6}}, +[11606]={{7077,2},{11371,10}}, +[11607]={{7077,4},{11371,26}}, +[11608]={{7077,4},{11371,18}}, +[11811]={{7078,1},{11382,1},{14343,3}}, +[11825]={{3860,6},{4394,1},{7077,1},{7191,1}}, +[11826]={{3860,2},{4389,2},{6037,1},{7075,1},{7191,1}}, +[12190]={{8831,3},{8925,1}}, +[12209]={{1015,1},{2678,1}}, +[12210]={{2692,1},{12184,1}}, +[12212]={{159,1},{4536,2},{12202,1}}, +[12213]={{2692,1},{12037,1}}, +[12214]={{2596,1},{12037,1}}, +[12215]={{159,1},{3713,1},{12204,2}}, +[12216]={{2692,2},{12206,1}}, +[12217]={{2692,1},{4402,1},{12037,1}}, +[12218]={{3713,2},{12207,1}}, +[12224]={{2678,1},{12223,1}}, +[12259]={{1206,1},{3466,2},{3859,10},{4234,1},{7067,1}}, +[12260]={{3577,4},{3859,10},{4234,2},{7068,2}}, +[12359]={{10620,1}}, +[12360]={{12359,1},{12363,1}}, +[12404]={{12365,1}}, +[12405]={{11188,4},{12359,16},{12361,1}}, +[12406]={{11186,4},{12359,12}}, +[12408]={{11184,4},{12359,12}}, +[12409]={{8170,8},{11185,4},{12359,20}}, +[12410]={{7910,1},{11188,4},{12359,24}}, +[12414]={{11186,4},{12359,26}}, +[12415]={{7077,2},{7910,1},{12359,18}}, +[12416]={{7077,2},{12359,10}}, +[12417]={{7077,4},{12359,18}}, +[12418]={{7077,4},{12359,18}}, +[12419]={{7077,4},{12359,14}}, +[12420]={{7077,4},{12359,20}}, +[12422]={{7910,2},{12359,40}}, +[12424]={{7909,1},{8170,6},{12359,22}}, +[12425]={{7910,1},{12359,20}}, +[12426]={{7909,1},{7910,1},{12359,34}}, +[12427]={{7910,2},{12359,34}}, +[12428]={{3864,2},{8170,6},{12359,24}}, +[12429]={{7910,2},{12359,44}}, +[12610]={{3577,6},{12359,20},{12360,2}}, +[12611]={{2842,10},{12359,20},{12360,2}}, +[12612]={{6037,2},{12359,30},{12360,2},{12364,1}}, +[12613]={{7910,1},{12359,40},{12360,2}}, +[12614]={{7910,1},{12359,40},{12360,2}}, +[12618]={{7076,4},{7080,4},{12360,8},{12364,2},{12655,24},{12800,2}}, +[12619]={{7080,6},{12360,10},{12361,2},{12364,1},{12655,20}}, +[12620]={{7076,6},{12360,6},{12655,16},{12799,2},{12800,1}}, +[12624]={{8153,4},{12359,40},{12364,1},{12655,2},{12803,4}}, +[12625]={{7080,2},{12359,20},{12360,4},{12364,2}}, +[12628]={{7910,4},{12359,40},{12361,4},{12662,10}}, +[12631]={{7078,2},{7910,4},{12359,20},{12655,6}}, +[12632]={{7080,4},{12359,20},{12361,4},{12655,4}}, +[12633]={{3577,6},{6037,6},{12359,20},{12655,4},{12800,2}}, +[12636]={{8168,60},{12359,40},{12364,2},{12655,4},{12799,6}}, +[12639]={{7076,10},{12360,15},{12361,4},{12655,20},{12799,4}}, +[12640]={{8146,40},{12359,80},{12360,12},{12361,10},{12800,4}}, +[12641]={{12360,30},{12364,6},{12655,30},{12800,6}}, +[12643]={{12365,1},{14047,1}}, +[12644]={{12365,4}}, +[12645]={{7076,2},{12359,4},{12644,4}}, +[12655]={{11176,3},{12359,1}}, +[12764]={{8170,4},{12359,16},{12644,2}}, +[12769]={{8153,6},{8170,8},{12359,30},{12644,2},{12799,6},{12803,6}}, +[12772]={{3577,4},{6037,2},{8170,4},{12359,30},{12361,2}}, +[12773]={{8170,4},{12359,20},{12644,2},{12799,2}}, +[12774]={{7910,4},{8170,4},{12359,30},{12361,4},{12644,2},{12655,4}}, +[12775]={{8170,6},{12359,40},{12644,6}}, +[12776]={{8170,4},{12359,20},{12364,2},{12655,6},{12804,4}}, +[12777]={{7077,4},{7078,4},{12644,2},{12655,10},{12800,2}}, +[12779]={{8170,4},{12359,30},{12644,2},{12799,2}}, +[12781]={{12360,2},{12361,2},{12364,1},{12655,6},{12799,2},{12804,4}}, +[12782]={{8170,4},{12359,40},{12360,2},{12361,2},{12644,2},{12662,16},{12808,8}}, +[12783]={{7910,6},{12360,10},{12644,4},{12655,10},{12799,6},{12800,6},{12810,2}}, +[12784]={{12360,20},{12644,2},{12810,6}}, +[12790]={{12360,15},{12644,2},{12799,4},{12800,8},{12810,8},{12811,1}}, +[12792]={{7077,4},{7910,4},{8170,4},{12359,30}}, +[12794]={{7076,6},{12364,8},{12655,20},{12799,8},{12810,4}}, +[12795]={{7910,10},{12360,10},{12644,2},{12655,10},{12662,8}}, +[12796]={{7076,10},{12359,50},{12360,15},{12809,4},{12810,6}}, +[12797]={{7080,4},{12360,18},{12361,8},{12644,2},{12800,8},{12810,4}}, +[12798]={{12359,40},{12360,12},{12364,8},{12644,2},{12808,10},{12810,4}}, +[12802]={{12364,2},{12644,2},{12655,20},{12800,2},{12804,20}}, +[12803]={{7076,1}}, +[12808]={{7080,1}}, +[12810]={{8170,1},{16202,1}}, +[13423]={{3372,1},{13422,1}}, +[13442]={{8846,3},{8925,1}}, +[13443]={{8838,2},{8839,2},{8925,1}}, +[13444]={{8925,1},{13463,3},{13467,2}}, +[13445]={{8838,1},{8925,1},{13423,2}}, +[13446]={{8925,1},{13464,2},{13465,1}}, +[13447]={{8925,1},{13463,1},{13466,2}}, +[13452]={{8925,1},{13465,2},{13466,2}}, +[13453]={{8846,2},{8925,1},{13466,2}}, +[13454]={{8925,1},{13463,3},{13465,1}}, +[13455]={{8925,1},{10620,1},{13423,3}}, +[13456]={{7070,1},{8925,1},{13463,1}}, +[13457]={{7068,1},{8925,1},{13463,1}}, +[13458]={{7067,1},{8925,1},{13463,1}}, +[13459]={{3824,1},{8925,1},{13463,1}}, +[13460]={{7069,1},{8925,1},{13464,1}}, +[13461]={{8925,1},{11176,1},{13463,1}}, +[13462]={{8925,1},{13466,2},{13467,2}}, +[13503]={{7076,8},{7078,8},{7080,8},{7082,8},{9262,2},{12803,8},{13468,4}}, +[13506]={{8925,1},{13423,30},{13465,10},{13468,1}}, +[13510]={{8846,30},{8925,1},{13423,10},{13468,1}}, +[13511]={{8925,1},{13463,30},{13467,10},{13468,1}}, +[13512]={{8925,1},{13463,30},{13465,10},{13468,1}}, +[13513]={{8925,1},{13465,10},{13467,30},{13468,1}}, +[13851]={{2692,1},{12203,1}}, +[13856]={{14048,3},{14341,1}}, +[13857]={{14048,5},{14227,1},{14341,1}}, +[13858]={{14048,5},{14227,1},{14341,1}}, +[13860]={{14048,4},{14227,1},{14341,1}}, +[13863]={{8170,4},{14048,4},{14341,1}}, +[13864]={{8170,4},{14048,4},{14227,2},{14341,1}}, +[13865]={{14048,6},{14227,2},{14341,1}}, +[13866]={{14048,4},{14227,2},{14341,1}}, +[13867]={{8170,4},{14048,7},{14227,2},{14341,1}}, +[13868]={{7079,2},{14048,5},{14341,1}}, +[13869]={{7079,2},{14048,5},{14341,1}}, +[13870]={{7080,1},{14048,3},{14341,1}}, +[13871]={{7080,1},{14048,6},{14341,1}}, +[13927]={{3713,1},{13754,1}}, +[13928]={{3713,1},{13755,1}}, +[13929]={{2692,2},{13756,1}}, +[13930]={{13758,1}}, +[13931]={{159,1},{13759,1}}, +[13932]={{13760,1}}, +[13933]={{159,1},{13888,1}}, +[13934]={{2692,1},{3713,1},{13893,1}}, +[13935]={{3713,1},{13889,1}}, +[14042]={{7077,3},{14048,5},{14341,1}}, +[14043]={{7077,3},{14048,4},{14341,1}}, +[14044]={{7078,1},{14048,5},{14341,1}}, +[14045]={{7078,1},{14048,6},{14341,1}}, +[14046]={{8170,2},{14048,5},{14341,1}}, +[14048]={{14047,5}}, +[14100]={{3577,2},{14048,5},{14341,1}}, +[14101]={{3577,2},{14048,4},{14341,1}}, +[14103]={{3577,2},{14048,4},{14341,1}}, +[14104]={{3577,4},{14048,6},{14227,1},{14341,1}}, +[14106]={{12662,4},{14048,8},{14256,8},{14341,2}}, +[14107]={{14048,5},{14256,4},{14341,1}}, +[14108]={{8170,4},{14048,6},{14256,4},{14341,1}}, +[14111]={{14048,5},{14256,4},{14341,1}}, +[14112]={{8170,4},{12662,4},{14048,7},{14256,6},{14341,2}}, +[14128]={{11176,2},{14048,8},{14341,1}}, +[14130]={{7910,1},{11176,4},{14048,6},{14341,1}}, +[14132]={{11176,1},{14048,6},{14341,1}}, +[14134]={{7068,4},{7077,4},{7078,4},{14048,6},{14341,1}}, +[14136]={{7080,4},{12808,4},{14048,10},{14256,12},{14341,1}}, +[14137]={{14048,6},{14341,1},{14342,4}}, +[14138]={{14048,6},{14341,1},{14342,4}}, +[14139]={{14048,5},{14341,1},{14342,5}}, +[14140]={{12800,1},{12810,2},{14048,4},{14341,2},{14342,6}}, +[14141]={{9210,4},{14048,6},{14227,1},{14341,1}}, +[14142]={{9210,2},{14048,4},{14227,1},{14341,1}}, +[14143]={{9210,2},{14048,3},{14227,1},{14341,1}}, +[14144]={{9210,4},{14048,6},{14341,1}}, +[14146]={{9210,10},{12364,6},{12810,8},{13926,6},{14048,10},{14341,2},{14342,10}}, +[14152]={{7076,10},{7078,10},{7080,10},{7082,10},{14048,12},{14341,2}}, +[14153]={{7078,12},{12662,20},{12808,12},{14048,12},{14256,40},{14341,2}}, +[14154]={{9210,10},{12811,4},{13926,4},{14048,12},{14341,2},{14342,10}}, +[14155]={{14048,4},{14341,1},{14342,1}}, +[14156]={{14048,8},{14341,2},{14342,12},{14344,2},{17012,2}}, +[14342]={{14256,2}}, +[14529]={{14047,1}}, +[14530]={{14047,2}}, +[15045]={{8170,20},{14341,2},{15412,25}}, +[15046]={{8170,20},{14341,1},{15412,25}}, +[15047]={{8170,40},{14341,1},{15414,30}}, +[15048]={{8170,28},{14341,1},{15407,1},{15415,30}}, +[15049]={{8170,28},{12810,2},{14341,1},{15407,1},{15415,30}}, +[15050]={{8170,40},{14341,2},{15407,1},{15416,60}}, +[15051]={{8170,44},{12810,2},{14341,1},{15407,1},{15416,45}}, +[15052]={{8170,40},{12810,4},{14341,2},{15407,1},{15416,60}}, +[15053]={{7076,1},{7078,1},{8170,8},{14341,1}}, +[15054]={{7075,1},{7078,1},{8170,6},{14341,1}}, +[15055]={{7076,1},{7078,1},{8170,10},{14341,2}}, +[15056]={{7080,3},{7082,3},{8170,16},{14341,1},{15407,1}}, +[15057]={{7080,2},{7082,2},{8170,16},{14341,1}}, +[15058]={{7080,3},{7082,3},{8170,12},{12810,2},{14341,1}}, +[15059]={{8170,16},{12803,8},{14341,2},{14342,2},{15407,1}}, +[15060]={{8170,16},{12803,6},{14341,1},{15407,1}}, +[15061]={{8170,12},{12803,4},{14341,1}}, +[15062]={{8170,30},{14341,1},{15407,1},{15417,14}}, +[15063]={{8170,30},{14341,1},{15417,8}}, +[15064]={{8170,28},{14341,1},{15419,12}}, +[15065]={{8170,24},{14341,1},{15419,14}}, +[15066]={{1529,1},{8170,40},{14341,1},{15407,1},{15420,120}}, +[15067]={{1529,2},{8170,24},{14341,1},{15420,80}}, +[15068]={{8170,12},{14341,2},{15407,1},{15422,12}}, +[15069]={{8170,6},{14341,1},{15422,8}}, +[15070]={{8170,6},{14341,1},{15422,10}}, +[15071]={{8170,4},{14341,1},{15422,6}}, +[15072]={{8170,8},{14341,1},{15423,8}}, +[15073]={{8170,4},{14341,1},{15423,8}}, +[15074]={{8170,6},{14341,1},{15423,6}}, +[15075]={{8170,10},{14341,1},{15423,10}}, +[15076]={{8170,6},{14341,1},{15408,6}}, +[15077]={{8170,4},{14341,1},{15408,4}}, +[15078]={{8170,6},{14341,1},{15408,8}}, +[15079]={{8170,8},{14341,1},{15408,12}}, +[15080]={{8170,8},{14341,1},{15407,1},{15408,12}}, +[15081]={{8170,14},{14341,2},{15407,1},{15408,14}}, +[15082]={{8170,6},{14341,1},{15408,8}}, +[15083]={{2325,1},{8170,8},{14341,1}}, +[15084]={{2325,1},{8170,8},{14341,1}}, +[15085]={{2325,4},{8170,20},{14256,6},{14341,2},{15407,2}}, +[15086]={{2325,1},{8170,12},{14341,1}}, +[15087]={{2325,3},{8170,16},{14341,1},{15407,1}}, +[15088]={{2325,2},{8170,14},{14341,2}}, +[15090]={{8170,22},{12810,4},{14047,16},{14341,2},{15407,1}}, +[15091]={{8170,10},{14047,6},{14341,1}}, +[15092]={{7971,1},{8170,6},{14047,6},{14341,1}}, +[15093]={{8170,12},{14047,10},{14341,1}}, +[15094]={{8170,14},{14047,10},{14341,1}}, +[15095]={{8170,18},{12810,2},{14047,12},{14341,1}}, +[15096]={{8170,16},{12810,4},{14047,18},{14341,2},{15407,1}}, +[15138]={{14044,1},{14341,1},{15410,1}}, +[15141]={{8170,40},{14341,2},{15410,12},{15416,60}}, +[15407]={{8171,1},{15409,1}}, +[15564]={{8170,5}}, +[15802]={{7971,2},{14048,6},{14341,1},{14342,4}}, +[15846]={{10558,1},{10560,4},{10561,1},{12359,6}}, +[15869]={{2842,1},{3470,1}}, +[15870]={{3486,1},{3577,1}}, +[15871]={{6037,1},{7966,1}}, +[15872]={{12360,1},{12644,1}}, +[15992]={{12365,2}}, +[15993]={{12359,3},{14047,3},{15992,3},{15994,1}}, +[15994]={{12359,3},{14047,1}}, +[15995]={{10546,1},{10559,2},{10561,2},{12359,4},{15994,2}}, +[15996]={{8170,1},{10558,1},{12803,1},{15994,4}}, +[15997]={{12359,2},{15992,1}}, +[15999]={{7910,4},{10502,1},{12810,2},{14047,8}}, +[16000]={{12359,6}}, +[16004]={{8170,4},{10546,2},{11371,6},{12361,2},{12799,2},{16000,2}}, +[16005]={{11371,1},{14047,3},{15992,3},{15994,2}}, +[16006]={{12360,1},{14227,1}}, +[16007]={{7076,2},{7078,2},{12360,10},{12800,2},{12810,2},{16000,2}}, +[16008]={{10500,1},{12364,2},{12810,4}}, +[16009]={{10558,1},{12799,1},{15994,1},{16006,2}}, +[16022]={{10558,4},{10576,1},{12655,10},{12810,6},{15994,6},{16006,8}}, +[16023]={{6037,1},{8170,2},{10561,1},{14047,4},{15994,2},{16000,1}}, +[16040]={{12359,3},{14047,1},{16006,1}}, +[16206]={{12360,3},{12644,1}}, +[16207]={{13926,1},{14343,4},{14344,2},{16203,4},{16204,10},{16206,1}}, +[16766]={{1179,1},{2692,1},{7974,2}}, +[16979]={{7078,4},{12810,2},{14048,8},{14341,2},{17010,6}}, +[16980]={{12810,6},{14048,12},{14341,2},{17010,4},{17011,4}}, +[16982]={{14341,2},{17010,6},{17011,2},{17012,20}}, +[16983]={{14341,2},{17010,3},{17011,6},{17012,15}}, +[16984]={{12810,6},{14341,2},{15416,30},{17010,4},{17011,3}}, +[16988]={{11371,16},{17010,4},{17011,5}}, +[16989]={{11371,6},{17010,3},{17011,3}}, +[17013]={{11371,16},{17010,4},{17011,6}}, +[17014]={{11371,4},{17010,2},{17011,2}}, +[17015]={{11371,16},{11382,2},{12810,2},{17010,12}}, +[17016]={{11371,18},{11382,2},{12810,2},{17011,12}}, +[17193]={{7078,25},{11371,20},{11382,10},{12360,50},{17010,10},{17011,10},{17203,8}}, +[17197]={{6889,1},{17194,1}}, +[17198]={{1179,1},{6889,1},{17194,1},{17196,1}}, +[17222]={{12205,2}}, +[17704]={{3829,1},{3859,10},{4234,2},{7069,2},{7070,2}}, +[17708]={{3358,1},{3372,1},{3819,2}}, +[17716]={{3829,1},{3860,8},{4389,4},{17202,4}}, +[17721]={{4234,8},{4291,1},{7067,4}}, +[17723]={{2605,4},{4291,1},{4305,5}}, +[17771]={{12360,10},{17010,1},{18562,1},{18567,3}}, +[17967]={{15410,1}}, +[17968]={{16203,2},{16204,2},{17967,1}}, +[18045]={{3713,1},{12208,1}}, +[18168]={{7076,8},{7082,8},{12360,6},{12803,12},{16006,2}}, +[18232]={{7067,2},{7068,1},{7191,1},{8170,4},{12359,12}}, +[18238]={{1210,4},{4236,2},{4304,6},{7428,8},{7971,2},{8343,1}}, +[18251]={{14341,2},{17012,3}}, +[18253]={{10286,1},{13463,4},{13464,4},{18256,1}}, +[18254]={{3713,1},{18255,1}}, +[18258]={{8170,4},{14048,2},{14341,1},{18240,1}}, +[18262]={{7067,2},{12365,3}}, +[18263]={{7078,2},{12810,6},{14341,4},{14342,6},{17010,8}}, +[18282]={{12360,6},{16000,2},{16006,2},{17010,4},{17011,2}}, +[18283]={{7076,2},{11371,6},{16000,1},{16006,4},{17011,2}}, +[18294]={{7972,1},{8831,2},{8925,1}}, +[18405]={{7078,12},{7080,12},{9210,10},{14048,16},{14341,6},{14342,10},{14344,6}}, +[18407]={{12662,6},{12808,8},{14048,12},{14256,20},{14341,2}}, +[18408]={{7078,10},{7910,2},{14048,12},{14341,2}}, +[18409]={{13926,2},{14048,12},{14341,2},{14342,6}}, +[18413]={{12360,1},{12809,4},{14048,12},{14341,2}}, +[18486]={{13926,2},{14048,6},{14341,2},{14342,4}}, +[18504]={{8170,12},{12804,12},{14341,4},{15407,2}}, +[18506]={{7082,6},{8170,12},{11754,4},{14341,4},{15407,2}}, +[18508]={{8170,12},{14341,4},{15407,4},{15420,60},{18512,8}}, +[18509]={{8170,30},{12607,12},{14341,8},{15407,5},{15414,30},{15416,30}}, +[18510]={{7080,10},{8170,30},{12803,12},{14341,8},{15407,3},{18512,8}}, +[18511]={{7082,12},{8170,30},{12753,4},{12809,8},{14341,8},{15407,4}}, +[18587]={{7191,2},{7910,2},{14227,2},{15994,2},{18631,2}}, +[18588]={{4338,2},{10505,1}}, +[18594]={{159,1},{8170,2},{15992,3},{15994,2}}, +[18631]={{6037,2},{7067,2},{7069,1}}, +[18634]={{3829,2},{7078,4},{12361,2},{13467,4},{15994,6},{18631,2}}, +[18637]={{14047,2},{16000,2},{18631,1}}, +[18638]={{7080,6},{7910,4},{11371,4},{12800,2},{18631,3}}, +[18639]={{11371,8},{12799,2},{12800,2},{12803,6},{12808,4},{18631,4}}, +[18641]={{14047,3},{15992,2}}, +[18645]={{7191,1},{7910,1},{8170,4},{12359,4},{15994,2}}, +[18660]={{3864,1},{10558,1},{10560,1},{10561,1},{15994,2}}, +[18662]={{2321,1},{4234,2}}, +[18948]={{4234,8},{4236,2},{4461,1},{5498,4},{5637,4}}, +[18984]={{3860,10},{7077,4},{7910,2},{10586,1},{18631,1}}, +[18986]={{3860,12},{7075,4},{7079,2},{7909,4},{9060,1},{18631,2}}, +[19026]={{8150,1},{14047,2},{15992,2}}, +[19043]={{7076,3},{12359,12},{12803,3}}, +[19044]={{8170,30},{12803,4},{12804,2},{14341,2},{15407,2}}, +[19047]={{7076,3},{12803,3},{14048,8},{14227,2}}, +[19048]={{7076,6},{12360,4},{12803,6}}, +[19049]={{12803,6},{12804,6},{12810,8},{14227,2},{15407,2}}, +[19050]={{7076,5},{12803,5},{14227,2},{14342,5}}, +[19051]={{6037,6},{12359,8},{12811,1}}, +[19052]={{7080,4},{8170,30},{12809,2},{14341,2},{15407,2}}, +[19056]={{12809,2},{12810,4},{13926,2},{14048,6},{14227,2}}, +[19057]={{6037,10},{12360,2},{12811,1}}, +[19058]={{12803,4},{12809,4},{12810,8},{14341,2},{15407,2}}, +[19059]={{12809,2},{14227,2},{14342,5}}, +[19148]={{11371,4},{17010,2},{17011,4}}, +[19149]={{14227,4},{15407,4},{17011,5}}, +[19156]={{7078,6},{14227,4},{14342,10},{17010,2},{17011,3}}, +[19157]={{12607,4},{14227,4},{15407,4},{17010,5},{17011,2},{17012,4}}, +[19162]={{12810,10},{14227,4},{15407,4},{17010,8},{17012,12}}, +[19163]={{7076,6},{14227,4},{15407,4},{17010,2},{17011,7}}, +[19164]={{11371,4},{11382,2},{17010,5},{17011,3},{17012,4}}, +[19165]={{7078,10},{14227,4},{14342,8},{17010,5},{17011,3}}, +[19166]={{11371,4},{11382,1},{12360,12},{17010,6},{17011,3}}, +[19167]={{11371,6},{12360,16},{17010,2},{17011,5}}, +[19168]={{11371,6},{12360,10},{12809,12},{17010,6},{17011,6}}, +[19169]={{11371,12},{12360,10},{12364,4},{17010,5},{17011,8}}, +[19170]={{11371,8},{12360,12},{12800,4},{17010,7},{17011,4}}, +[19440]={{19441,1}}, +[19682]={{12804,4},{14048,4},{14227,2},{14342,3},{19726,5}}, +[19683]={{12804,4},{14048,4},{14227,2},{14342,4},{19726,4}}, +[19684]={{12810,4},{14048,4},{14227,4},{14342,3},{19726,3}}, +[19685]={{12803,4},{14341,4},{15407,5},{19767,14}}, +[19686]={{12803,4},{14341,3},{15407,4},{19767,10}}, +[19687]={{12803,4},{14341,3},{15407,3},{19767,8}}, +[19688]={{14341,3},{15407,3},{19726,2},{19768,35}}, +[19689]={{14341,3},{15407,3},{19726,2},{19768,25}}, +[19690]={{7910,2},{12359,20},{19726,2},{19774,10}}, +[19691]={{7910,1},{12359,16},{19726,2},{19774,8}}, +[19692]={{12359,12},{12810,4},{19726,2},{19774,6}}, +[19693]={{12359,20},{12799,2},{19774,14}}, +[19694]={{12359,18},{12799,2},{19774,12}}, +[19695]={{12359,16},{12799,1},{19774,10}}, +[19931]={{12804,6},{12938,1},{13468,1},{19943,1}}, +[19998]={{12804,8},{12810,4},{16006,1},{19726,5},{19774,5}}, +[19999]={{12804,8},{12810,4},{16006,2},{19726,4},{19774,5}}, +[20002]={{8925,1},{13463,2},{13464,1}}, +[20004]={{8846,1},{8925,1},{13466,2}}, +[20007]={{8925,1},{13463,1},{13466,2}}, +[20008]={{8925,1},{10286,2},{13465,2},{13467,2}}, +[20039]={{11371,6},{17010,3},{17011,3},{17012,4}}, +[20074]={{3667,2},{3713,1}}, +[20295]={{8170,28},{14341,2},{15407,2},{15415,36}}, +[20296]={{8170,20},{14341,2},{15407,1},{15412,30}}, +[20380]={{12803,4},{12810,12},{14227,6},{15407,4},{20381,6}}, +[20452]={{3713,1},{20424,1}}, +[20476]={{18512,2},{20498,20},{20501,1}}, +[20477]={{15407,1},{18512,2},{20498,30},{20501,2}}, +[20478]={{15407,2},{18512,2},{20498,40},{20501,3}}, +[20479]={{7078,2},{15407,2},{20498,40},{20500,3}}, +[20480]={{7078,2},{15407,1},{20498,30},{20500,2}}, +[20481]={{7078,2},{20498,20},{20500,1}}, +[20537]={{12810,2},{14048,4},{14227,2},{14256,4},{20520,6}}, +[20538]={{14048,6},{14227,2},{14256,6},{20520,8}}, +[20539]={{12810,2},{14048,2},{14227,2},{14256,2},{20520,6}}, +[20549]={{6037,6},{12359,12},{12810,2},{20520,6}}, +[20550]={{6037,10},{12359,20},{20520,10}}, +[20551]={{6037,8},{11754,1},{12359,16},{20520,8}}, +[20575]={{2319,8},{2321,2},{4231,1},{7286,8}}, +[20744]={{3371,1},{10940,2},{17034,1}}, +[20745]={{3372,1},{11083,3},{17034,2}}, +[20746]={{3372,1},{11137,3},{17035,2}}, +[20747]={{8831,2},{8925,1},{11176,3}}, +[20748]={{8831,3},{14344,2},{18256,1}}, +[20749]={{4625,3},{14344,2},{18256,1}}, +[20750]={{4625,2},{8925,1},{16204,3}}, +[20844]={{5173,7},{8925,1}}, +[21023]={{2692,1},{8150,1},{9061,1},{21024,1}}, +[21072]={{2678,1},{21071,1}}, +[21154]={{2604,2},{4625,2},{14048,4},{14341,1}}, +[21217]={{2692,1},{21153,1}}, +[21277]={{7079,2},{10558,1},{15407,1},{15994,4},{18631,2}}, +[21278]={{7080,4},{7082,4},{12810,6},{14227,2},{15407,2}}, +[21340]={{7972,2},{8170,4},{14048,6},{14341,1}}, +[21341]={{12810,6},{14227,4},{14256,12},{20520,2}}, +[21342]={{7078,4},{14227,4},{14256,20},{17012,16},{19726,8}}, +[21542]={{2604,2},{4625,2},{14048,4},{14341,1}}, +[21546]={{4625,3},{6371,3},{8925,1}}, +[21557]={{2319,1},{4364,1}}, +[21558]={{2319,1},{4364,1}}, +[21559]={{2319,1},{4364,1}}, +[21569]={{9060,1},{9061,1},{10560,1},{10561,1}}, +[21570]={{9060,4},{9061,4},{10561,1},{18631,2}}, +[21571]={{4304,1},{10505,1}}, +[21574]={{4304,1},{10505,1}}, +[21576]={{4304,1},{10505,1}}, +[21589]={{4234,1},{4377,1}}, +[21590]={{4234,1},{4377,1}}, +[21592]={{4234,1},{4377,1}}, +[21714]={{8170,1},{15992,1}}, +[21716]={{8170,1},{15992,1}}, +[21718]={{8170,1},{15992,1}}, +[22191]={{12800,4},{12809,10},{12810,12},{22202,36},{22203,15}}, +[22194]={{12810,8},{13512,1},{22202,24},{22203,8}}, +[22195]={{12810,4},{22202,14}}, +[22196]={{7076,10},{12364,4},{12655,12},{22202,40},{22203,18}}, +[22197]={{7076,2},{12655,4},{22202,14}}, +[22198]={{7076,4},{12655,8},{22202,24},{22203,8}}, +[22246]={{4339,4},{8343,2},{11137,4}}, +[22248]={{14048,5},{14341,2},{16203,2}}, +[22249]={{12810,4},{14048,6},{14227,4},{14344,4}}, +[22251]={{8831,10},{11040,8},{14048,5},{14341,2}}, +[22252]={{13468,1},{14048,6},{14227,4},{14342,2}}, +[22383]={{12360,12},{12810,4},{13512,2},{20725,2}}, +[22384]={{11371,10},{12360,15},{12753,2},{12808,20},{15417,10},{20520,20}}, +[22385]={{7076,10},{12360,12},{12655,20},{13510,2}}, +[22652]={{7080,6},{14048,8},{14227,8},{22682,7}}, +[22654]={{7080,4},{14048,4},{14227,4},{22682,5}}, +[22655]={{7080,2},{14048,2},{14227,4},{22682,4}}, +[22658]={{7080,2},{14048,4},{14227,4},{22682,5}}, +[22660]={{12803,4},{14227,4},{14342,2},{19726,1}}, +[22661]={{7080,2},{12810,16},{14227,4},{15407,4},{22682,7}}, +[22662]={{7080,2},{12810,12},{14227,4},{15407,3},{22682,5}}, +[22663]={{7080,2},{12810,12},{14227,4},{15407,2},{22682,4}}, +[22664]={{7080,2},{14227,4},{15407,4},{15408,24},{22682,7}}, +[22665]={{7080,2},{14227,4},{15407,2},{15408,16},{22682,4}}, +[22666]={{7080,2},{14227,4},{15407,3},{15408,16},{22682,5}}, +[22669]={{7080,4},{12359,16},{12360,2},{22682,7}}, +[22670]={{7080,2},{12359,12},{12360,2},{22682,5}}, +[22671]={{7080,2},{12359,12},{12360,2},{22682,4}}, +[22756]={{12803,2},{14048,4},{14227,2},{19726,2}}, +[22757]={{12803,2},{14048,4},{14227,2},{14342,2}}, +[22758]={{12803,4},{14048,2},{14227,2}}, +[22759]={{12803,2},{12810,12},{15407,2},{19726,2}}, +[22760]={{12803,2},{12810,6},{15407,2},{18512,2}}, +[22761]={{12803,2},{12810,4},{15407,1}}, +[22762]={{12360,2},{12655,12},{12803,2},{19726,2}}, +[22763]={{12655,8},{12803,2},{19726,1}}, +[22764]={{12655,6},{12803,2}}, +[30818]={{159,1},{2674,1}}, +[41308]={{55174,1},{55246,1},{2880,2},{55151,2}}, +[41309]={{2841,2},{81094,3},{55150,4},{55245,1}}, +[41310]={{2841,6},{2880,2},{55245,2}}, +[41311]={{55174,1},{1210,2},{55246,1}}, +[41312]={{2841,6},{1210,3},{785,1},{55246,1}}, +[41313]={{2841,6},{55246,1},{55151,4}}, +[41314]={{2841,6},{1206,1},{3466,2},{55151,2}}, +[41315]={{2841,8},{1705,3},{3466,1},{55246,1}}, +[41316]={{55174,1},{1705,2},{3385,1}}, +[41318]={{2842,3},{10998,1},{1210,1}}, +[41319]={{2842,2}}, +[41320]={{2771,2},{55151,2}}, +[41321]={{12359,2}}, +[41322]={{3860,2}}, +[41323]={{41322,1},{7077,1},{55250,4},{55247,2}}, +[41324]={{3860,10},{7971,3},{7909,1},{55247,1}}, +[41325]={{2842,5},{3466,1},{55246,2},{55151,2}}, +[41326]={{1705,1},{2319,1},{2841,2},{4371,2},{4404,1}}, +[41327]={{3864,1},{4389,1},{7191,1},{10559,2},{10561,4}}, +[41328]={{4375,3},{4382,3},{4387,1},{4389,1},{55155,1}}, +[41329]={{41319,1},{3466,2},{55246,4},{55151,8},{55249,3},{1206,3}}, +[41330]={{12359,6},{13454,1},{12363,1}}, +[41331]={{3577,2}}, +[41332]={{3575,2}}, +[41340]={{3577,8},{55246,3},{55152,3}}, +[41341]={{7911,2},{6037,2}}, +[41342]={{41332,1},{3575,2},{2838,6},{3355,1},{55152,2}}, +[41343]={{3860,8},{4234,2},{55152,2}}, +[41344]={{2772,3},{55152,3}}, +[41345]={{3860,14},{3577,4},{55251,2},{7909,2},{55153,4}}, +[41346]={{41322,1},{55251,1},{7909,1},{6149,2},{55247,1}}, +[41347]={{41341,1},{7067,1},{7075,1},{55153,2}}, +[41349]={{55250,5},{6371,3},{7077,1},{7068,1}}, +[41673]={{1179,1},{4539,1},{41677,1}}, +[41674]={{2677,1},{2692,1},{41675,1}}, +[46600]={{818,2},{2840,16},{3470,3}}, +[47408]={{5173,3},{8924,3},{8925,1}}, +[47409]={{5173,3},{8924,3},{8925,1}}, +[47410]={{13452,1},{18256,1},{61224,1}}, +[47412]={{9206,1},{13454,1},{18256,1}}, +[47414]={{12820,1},{18256,1},{61423,1}}, +[50237]={{8838,1},{8925,1},{10286,3},{13464,1}}, +[51256]={{2321,2},{2996,3},{6260,3}}, +[51262]={{730,1},{814,1},{3371,1}}, +[51264]={{818,2},{2840,12},{3470,2}}, +[51268]={{2589,2},{4357,2},{4359,4}}, +[51284]={{818,1},{2318,8},{2321,2},{4231,1}}, +[51312]={{1206,1},{2841,2},{4375,2},{10998,1}}, +[51313]={{1206,1},{2841,2},{4375,2},{10998,1}}, +[53015]={{159,1},{2692,1},{3667,1},{3713,1},{12037,2},{12202,1}}, +[54009]={{2931,3},{8924,2},{8925,1}}, +[54010]={{2931,4},{8924,3},{8925,1}}, +[55043]={{4480,10},{5117,15},{7081,20},{7082,8},{12810,12},{14341,4},{15407,6}}, +[55046]={{8925,1},{13467,3}}, +[55048]={{8831,3},{8925,1}}, +[55050]={{12810,10},{14341,6},{16203,4},{61673,4}}, +[55052]={{9210,5},{12361,2},{14048,12},{55048,5}}, +[55054]={{8165,20},{8170,30},{15412,5},{15414,5},{15415,5},{15416,5}}, +[55056]={{9210,10},{14048,8},{14341,1},{14342,3},{16204,40},{20725,1}}, +[55058]={{7080,6},{12360,3},{12655,12},{12799,4},{13926,4},{14341,8}}, +[55060]={{55252,1},{12360,2},{12655,16},{20725,4},{11291,20},{12800,6},{55248,4}}, +[55141]={{41332,1},{3577,2},{3864,4},{55246,1}}, +[55142]={{41332,1},{55249,2},{55151,2},{55246,1}}, +[55143]={{41331,1},{1529,1},{55246,2},{55152,2}}, +[55144]={{2841,6},{3577,1},{55249,1},{2880,1}}, +[55145]={{3575,8},{3577,2},{55249,2}}, +[55146]={{3577,5},{4234,2},{3466,2},{3825,1}}, +[55147]={{3575,12},{3577,4},{3864,4},{1206,2},{55247,1},{11135,1}}, +[55148]={{3575,12},{1529,2},{1705,2},{3357,2},{55247,1}}, +[55150]={{2589,1},{2835,1}}, +[55151]={{2592,2},{2836,2}}, +[55152]={{4306,3},{2838,3}}, +[55153]={{4338,3},{7912,3}}, +[55154]={{14047,3},{12365,3}}, +[55156]={{2840,2}}, +[55157]={{2840,4}}, +[55158]={{55156,1},{55245,1}}, +[55159]={{55156,1},{2840,1},{55150,2}}, +[55160]={{55156,1},{55245,1},{55150,4},{818,1}}, +[55161]={{2840,4},{55245,2},{774,1}}, +[55162]={{55156,1},{5997,1},{774,1}}, +[55163]={{55156,1},{55150,1},{55245,1},{818,1}}, +[55164]={{55156,1},{3382,1},{55245,1}}, +[55165]={{2840,10},{55150,4},{5498,2}}, +[55166]={{55156,1},{55150,1},{81094,1}}, +[55167]={{55156,1},{55245,1},{1210,1},{159,5}}, +[55168]={{55156,1},{774,1},{2880,1},{818,1}}, +[55169]={{55156,1},{5498,1},{55150,2},{55245,1}}, +[55170]={{55156,1},{81094,2},{2880,1},{55150,1}}, +[55171]={{2840,8},{81094,1},{818,1},{2880,1},{1210,1}}, +[55172]={{2840,6},{81094,3},{55245,1},{55150,4}}, +[55173]={{2840,8},{5498,1},{818,1},{2880,1},{55150,2}}, +[55174]={{2841,2}}, +[55175]={{2841,8},{1210,1},{81094,1},{55151,4},{55245,2}}, +[55176]={{55174,1},{2449,3},{2836,6},{2880,4}}, +[55178]={{3860,8},{55249,1},{55251,1},{55250,1}}, +[55180]={{12359,8},{55249,4},{8956,2},{55153,2}}, +[55195]={{3575,6},{3864,2},{55249,2},{55152,3}}, +[55196]={{3860,8},{7909,2},{55152,2}}, +[55197]={{3860,6},{7910,2},{55249,2},{55153,4}}, +[55198]={{3860,12},{55251,4},{55247,1}}, +[55199]={{12359,8},{12799,2},{55154,2},{55247,1}}, +[55200]={{12359,5},{12655,1},{55249,4},{12808,2},{55154,2}}, +[55202]={{3575,6},{3577,1},{55249,2},{3864,2}}, +[55204]={{6037,12},{7075,4},{7067,4},{7068,4},{55153,8}}, +[55210]={{3859,3},{1705,2},{55152,2}}, +[55211]={{3859,4},{1529,2},{55152,2}}, +[55212]={{3860,3},{55250,2},{55153,2}}, +[55213]={{3860,3},{9262,2},{55153,2}}, +[55228]={{41321,1},{55250,1},{7910,1},{7077,1},{55247,1}}, +[55241]={{12359,24},{12800,2},{12799,2},{7075,2},{55154,4}}, +[55242]={{3860,24},{3864,6},{55249,6},{7075,2},{55246,4}}, +[55243]={{3860,18},{55249,3},{55251,3},{3864,3},{55152,6}}, +[55244]={{8170,12},{10648,20},{16203,2},{12655,2},{7076,3}}, +[55248]={{11175,1},{16203,1},{55247,1}}, +[55255]={{41321,1},{55251,3},{7069,3}}, +[55256]={{41321,1},{3466,2},{55251,3},{55153,3}}, +[55258]={{41321,1},{12361,1},{55251,1},{55247,1}}, +[55259]={{41341,1},{12655,5},{12361,5},{7076,1},{55154,8},{55247,3}}, +[55260]={{41321,1},{7910,2},{55246,4},{55247,1}}, +[55261]={{41321,1},{7910,5},{55154,3},{55247,1}}, +[55263]={{56033,1},{12799,3},{20520,3},{55248,2}}, +[55264]={{41321,1},{12364,1},{55154,3},{55247,1}}, +[55265]={{41321,1},{12655,4},{12364,2},{55154,2},{55247,1}}, +[55266]={{12359,20},{7910,2},{55250,2},{3466,2},{55246,4}}, +[55267]={{12359,12},{55154,6},{12361,1},{3829,1}}, +[55268]={{6037,4},{12361,2},{55247,2},{7069,4}}, +[55269]={{12359,8},{55251,5},{55247,1}}, +[55271]={{12359,12},{6037,6},{7910,3},{7909,3},{3466,1}}, +[55272]={{12359,28},{3577,4},{7082,2},{61673,2},{55154,8}}, +[55273]={{12359,14},{55154,4},{3864,3},{55246,1}}, +[55316]={{55156,1},{2880,3},{55150,2}}, +[55317]={{55156,1},{2880,1},{2447,4}}, +[55318]={{55174,1},{1210,1},{81094,1},{55150,1}}, +[55319]={{55174,1},{5498,1},{2880,1}}, +[55320]={{55174,1},{1705,3},{55245,1}}, +[55321]={{55174,1},{5635,4},{55151,2}}, +[55322]={{41332,1},{7067,1},{3466,1},{55151,2}}, +[55323]={{41332,1},{55249,4},{3864,4},{3391,1},{3466,2}}, +[55324]={{41332,1},{11135,1},{55152,1},{3577,2}}, +[55325]={{41322,1},{7909,2},{1210,2},{55246,1}}, +[55326]={{2841,10},{1210,2},{2453,2}}, +[55327]={{2841,12},{3575,6},{7069,2},{7068,2},{3388,2},{55151,4}}, +[55328]={{3575,18},{2838,2},{3864,2}}, +[55329]={{2840,6},{2447,2},{55245,1}}, +[55330]={{3576,8},{2450,2},{55151,2}}, +[55331]={{3575,10},{1210,2},{5500,1},{5635,8},{3466,2}}, +[55332]={{3859,6},{4306,2},{3864,2}}, +[55333]={{2840,5},{81094,2},{55150,2}}, +[55334]={{2842,4},{1206,4},{55246,2},{55151,2},{3390,1},{5635,8}}, +[55335]={{3575,6},{3864,4},{55246,2}}, +[55336]={{3860,2},{3357,2},{7909,2}}, +[55337]={{2841,10},{1206,2},{55245,1}}, +[55338]={{3575,10},{1529,2},{55245,2},{1288,6},{3466,2}}, +[55339]={{3859,7},{55250,2},{55245,2}}, +[55340]={{2840,2},{774,1},{818,1},{81094,1},{1210,1}}, +[55341]={{2842,2},{5498,2},{55249,4},{55246,2},{55151,2}}, +[55359]={{56033,1},{55252,2},{12364,8},{12799,10},{55248,4},{7078,6}}, +[55360]={{56033,1},{55252,1},{12803,16},{18335,4},{12799,6},{55248,4}}, +[55361]={{12360,6},{12359,12},{55252,1},{13926,4},{12800,8},{55248,2}}, +[55362]={{12359,16},{5116,12},{12361,6},{12799,6},{55247,2}}, +[55363]={{3577,24},{55252,1},{7080,8},{7076,8},{12800,6},{55248,4}}, +[55364]={{3577,32},{7082,8},{12800,8},{8152,8},{55247,6}}, +[55365]={{12359,28},{55252,2},{12364,12},{7077,8},{61673,8},{18567,1},{55248,6}}, +[55366]={{12359,18},{12360,2},{12799,10},{61673,6},{55154,6},{55247,2}}, +[55367]={{3577,26},{55252,1},{17011,2},{12799,8},{55248,2}}, +[55368]={{3577,32},{55252,1},{12364,6},{12800,8},{3466,6},{55248,4}}, +[55518]={{14048,6},{14227,3},{14342,2},{61673,5}}, +[55519]={{14048,3},{14227,4},{14342,2},{61673,4}}, +[55520]={{14048,8},{14227,2},{14342,3},{61673,7}}, +[55521]={{14048,6},{14227,2},{14342,3},{61673,7}}, +[55522]={{12810,8},{14227,1},{15407,2},{61673,5}}, +[55523]={{12810,7},{14227,2},{15407,2},{61673,4}}, +[55524]={{12810,12},{14227,1},{15407,4},{61673,8}}, +[55525]={{12810,13},{14227,2},{15407,3},{61673,6}}, +[55526]={{12360,1},{12607,1},{15407,1},{61673,6}}, +[55527]={{12360,1},{12607,3},{15407,1},{61673,5}}, +[55528]={{12360,1},{12607,2},{15407,2},{61673,6}}, +[55529]={{12360,1},{12607,2},{15407,2},{61673,7}}, +[55530]={{12360,2},{12655,7},{61673,6}}, +[55531]={{12360,2},{12655,9},{61673,7}}, +[55532]={{12360,2},{12655,10},{61673,5}}, +[55533]={{12360,2},{12655,12},{61673,7}}, +[55534]={{13926,1},{14048,6},{14227,4},{14342,2},{61673,4}}, +[56000]={{55249,1},{55151,1},{55247,1}}, +[56001]={{1529,1},{11135,1},{55247,1}}, +[56002]={{3864,1},{55152,4},{55247,1}}, +[56003]={{7909,1},{55152,1},{55246,2},{55247,1}}, +[56004]={{55250,1},{55152,4},{55247,1}}, +[56005]={{55251,1},{11082,1},{55247,1},{55152,1}}, +[56006]={{7910,1},{55152,1},{11134,1},{55247,1}}, +[56007]={{12361,1},{16203,1},{55247,1}}, +[56008]={{12799,2},{55153,1},{55247,1}}, +[56009]={{12364,1},{55153,2},{55247,2}}, +[56010]={{12800,2},{55152,2},{55247,1}}, +[56011]={{12363,1},{61673,1},{55247,1}}, +[56012]={{11754,1},{55153,5},{55247,1}}, +[56013]={{18335,1},{16202,1},{55154,2},{55248,1}}, +[56014]={{55252,1},{14344,1},{8831,4},{7075,1},{55248,1}}, +[56015]={{12800,1},{7910,1},{16203,1},{55154,2},{55247,1}}, +[56016]={{12363,1},{12364,1},{14344,1},{55154,2},{55248,1}}, +[56017]={{12800,2},{55154,1},{55247,1}}, +[56018]={{12364,1},{7081,3},{55152,1},{55247,1}}, +[56019]={{10620,3},{55154,3}}, +[56020]={{3858,3},{55153,3}}, +[56023]={{41322,1},{7909,3},{6372,3},{55152,3}}, +[56031]={{56033,1},{55252,1},{12364,5},{7910,5},{12361,5},{55248,5}}, +[56032]={{56033,1},{7910,12},{55250,12},{7078,8},{7077,12},{55248,8}}, +[56033]={{3577,8},{12360,2},{3466,2}}, +[56034]={{55251,2},{1705,8},{55246,2},{55152,4}}, +[56035]={{12655,3},{7082,3},{7069,5},{7081,5}}, +[56036]={{3577,8},{3860,2},{4304,2},{55246,6}}, +[56037]={{2840,5},{55245,2},{55150,2}}, +[56038]={{2841,8},{2836,4},{55151,2}}, +[56039]={{2841,6},{2880,1},{10940,1}}, +[56040]={{2842,4},{55249,1},{55246,2},{55245,2}}, +[56041]={{41331,1},{1206,2},{3389,1}}, +[56042]={{3575,5},{1206,1},{55249,1},{1705,1}}, +[56043]={{3577,8},{3466,1},{55152,2}}, +[56044]={{2840,6},{818,1},{55246,1}}, +[56045]={{2841,5},{81094,2},{55245,1}}, +[56046]={{2841,4},{2842,2},{3384,1},{55151,3}}, +[56047]={{3575,4},{55249,5},{3466,2},{55152,2}}, +[56048]={{41322,1},{7909,4},{55246,2},{55152,2}}, +[56049]={{3860,12},{3864,2}}, +[56050]={{3577,10},{4234,4},{55251,2}}, +[56051]={{3860,4},{7070,4},{7909,1},{55246,1}}, +[56052]={{3860,18},{55249,3},{7070,2},{55153,6}}, +[56053]={{41331,1},{3577,2},{1529,6},{3821,6},{55247,2}}, +[56054]={{3860,10},{3466,2},{55152,4}}, +[56055]={{3860,4},{10593,1},{55246,1}}, +[56056]={{7910,1},{7068,2},{55247,1}}, +[56057]={{12361,1},{3819,4},{7070,1},{55247,1}}, +[56058]={{55251,1},{55152,5},{55247,1}}, +[56059]={{56033,1},{6037,8},{12800,10},{3466,6},{55154,6},{55247,6}}, +[56060]={{12360,8},{55250,12},{7078,6},{7077,6},{7068,6},{55248,2}}, +[56061]={{12360,4},{12655,12},{7910,8},{7078,4},{7068,12},{55247,6}}, +[56062]={{12360,6},{3577,28},{15416,32},{17010,4},{55154,8},{7078,6}}, +[56063]={{12655,20},{7082,8},{7080,8},{7076,8},{7078,8},{12803,8}}, +[56064]={{12360,8},{6037,8},{12800,12},{61673,4},{55154,12},{55248,4}}, +[56065]={{41321,1},{12799,6},{7076,6},{3356,6},{55154,8},{55247,3}}, +[56066]={{56033,1},{12361,5},{7080,2},{55247,1}}, +[56067]={{11371,4},{7077,8},{11382,2}}, +[56068]={{12359,8},{12799,4},{7081,6},{9187,1},{55153,4}}, +[56069]={{3860,20},{7971,2},{55249,4},{55251,4},{3466,8}}, +[56070]={{3860,8},{3466,6},{3864,4},{55152,4},{55246,4}}, +[56071]={{3860,12},{6037,8},{7971,4},{8839,2},{55251,4},{55153,8}}, +[56072]={{12655,12},{12363,4},{61673,8},{16203,2},{55248,2}}, +[56073]={{12359,8},{7971,2},{11175,2},{7067,4},{55153,4}}, +[56074]={{1206,1},{55151,2},{55246,2}}, +[56075]={{12799,1},{13466,1},{55247,2}}, +[56076]={{55252,1},{12363,1},{7076,2},{55248,1}}, +[56077]={{55252,1},{61673,3},{55248,3}}, +[56089]={{3860,16},{55152,4},{3466,1},{55246,4}}, +[56090]={{12359,6},{6037,3},{7910,1},{7909,1}}, +[56091]={{55174,1},{1210,2},{785,1},{55246,1}}, +[56092]={{12655,2},{7082,1},{7081,3},{55154,3}}, +[56093]={{41321,1},{12655,6},{7082,3},{7081,6},{55154,4},{55247,4}}, +[56094]={{56033,1},{12360,2},{12655,6},{20520,12},{7075,20},{55248,3}}, +[56095]={{41341,1},{55251,8},{7079,8},{7070,12},{3358,12},{55247,4}}, +[56096]={{12360,6},{6037,6},{12800,8},{61673,4},{55154,6},{55248,4}}, +[56112]={{3860,1},{55249,1},{55247,1}}, +[56113]={{3372,1},{4625,1},{7068,1}}, +[58112]={{4234,14},{4236,2},{4402,2},{5637,2},{7287,4},{55249,2}}, +[58134]={{3824,3},{3864,3},{4291,2},{4305,8},{4342,1},{7068,3}}, +[58304]={{11371,8},{12364,4},{12800,2},{18631,4},{61673,3}}, +[58305]={{7076,4},{7910,4},{11371,8},{12363,2},{12803,6},{18631,4}}, +[60007]={{3824,8},{11371,10},{12360,14},{12655,12},{61673,6}}, +[60008]={{11371,12},{12360,12},{12655,12},{22202,6},{61673,6}}, +[60009]={{11371,8},{12360,10},{12655,10},{12800,4},{61673,4}}, +[60010]={{11371,14},{12360,14},{12655,12},{18335,1},{22203,2},{61673,8}}, +[60098]={{4404,1},{10558,1},{10561,1}}, +[60099]={{814,2},{3829,1},{4375,6},{7191,1},{9449,1},{18631,1},{60098,1}}, +[60287]={{12359,12},{12655,2},{12810,2},{20520,2}}, +[60288]={{12359,20},{12361,1},{12655,2},{12810,1},{20520,2}}, +[60289]={{12359,24},{12655,6},{12800,1},{12810,2},{20520,4}}, +[60290]={{12359,24},{12644,1},{12655,4},{12810,2},{20520,4}}, +[60291]={{7080,1},{12359,16},{12655,2},{20520,2}}, +[60292]={{7076,1},{12359,14},{12655,2},{12810,1},{20520,3}}, +[60293]={{12364,1},{12644,6},{12655,25},{12808,10},{12810,2},{20520,8}}, +[60294]={{3860,14},{3864,2},{4278,10},{7966,4}}, +[60573]={{7078,1},{11371,1},{11754,6},{12359,16}}, +[60574]={{7078,2},{8170,6},{11371,1},{11754,12},{12359,24},{20520,2}}, +[60575]={{7078,2},{8170,4},{11371,1},{11754,8},{12359,20}}, +[60576]={{7078,2},{11754,5},{12359,12},{12810,1}}, +[60577]={{7078,8},{11371,8},{11754,18},{12359,40},{12810,8}}, +[60578]={{7077,7},{7078,2},{11754,5},{12359,12},{12810,3}}, +[60907]={{7069,16},{7082,6},{14048,10},{14344,2}}, +[60908]={{3577,2},{7076,6},{7082,6},{12359,40},{12810,6}}, +[60909]={{7076,6},{12810,6},{14048,10},{14344,1}}, +[60910]={{7076,1},{7082,2},{8170,20},{12810,8}}, +[60976]={{3713,1},{10286,1},{60955,1}}, +[60977]={{3713,1},{13467,1},{60955,1}}, +[60978]={{3713,1},{13464,2},{60955,1}}, +[61181]={{2459,1},{8846,1},{8925,1},{13465,2}}, +[61182]={{7081,1},{12359,8},{12644,2},{61673,1}}, +[61183]={{12810,3},{14341,2},{61673,3}}, +[61185]={{12360,16},{12644,10},{12800,6},{12810,6},{12811,6},{13926,6}}, +[61186]={{8846,24},{9210,10},{12810,1},{14048,14},{14344,1}}, +[61187]={{10548,2},{12655,8},{12800,1},{12810,10},{15994,6},{16006,8}}, +[61188]={{7076,8},{12803,4},{12810,12},{14341,8},{15407,4}}, +[61216]={{3859,1},{20381,1},{61198,1}}, +[61224]={{8925,1},{11176,1},{61198,1}}, +[61225]={{730,1},{8831,1},{8925,1},{13463,1}}, +[61229]={{8170,1},{20381,1},{61198,1}}, +[61230]={{14341,1},{20381,1},{61198,1}}, +[61356]={{12803,6},{12810,20},{15407,6},{61229,22}}, +[61357]={{12803,2},{12810,6},{15407,1},{61229,8}}, +[61358]={{7082,6},{12803,6},{12810,12},{15407,4},{61229,12}}, +[61359]={{7082,8},{12810,12},{14341,8},{15407,2},{61229,8}}, +[61360]={{7082,6},{12803,6},{12810,4},{14048,40},{14342,6},{61230,20}}, +[61361]={{7080,4},{7082,4},{12803,4},{14048,24},{14342,4},{61230,14}}, +[61362]={{7080,2},{7082,2},{14342,2},{61230,8}}, +[61363]={{7082,4},{12803,4},{14048,12},{14342,4},{61230,8}}, +[61364]={{12360,8},{12644,8},{12799,8},{12800,2},{12810,12},{61216,20}}, +[61365]={{12364,8},{12655,8},{12810,8},{61216,14}}, +[61366]={{12644,4},{12655,4},{12810,4},{61216,8}}, +[61367]={{12644,2},{12655,8},{12800,2},{12810,8},{61216,8}}, +[61648]={{7076,3},{7078,3},{12359,16}}, +[61649]={{7076,6},{7078,6},{12360,2},{12655,12}}, +[61732]={{12803,80},{13468,5},{20725,10},{61197,5},{61199,25},{61673,25}}, +[61779]={{2840,8},{2880,1},{3470,2}}, +[61780]={{2841,8},{2880,1},{3478,2}}, +[61781]={{3486,2},{3575,8},{7071,1}}, +[61782]={{3860,8},{6037,1},{7071,1},{7966,2}}, +[61783]={{6037,1},{7071,1},{12359,8},{12644,2}}, +[61784]={{7071,1},{11754,1},{12360,2},{12361,1},{12644,2}}, +[61785]={{7076,1},{12644,2},{12803,1},{61216,2}}, +[61810]={{8846,6},{12359,10},{19933,6}}, +[61818]={{11382,1},{7077,6},{55248,1},{55154,2}}, +[65000]={{8170,35},{12803,6},{12810,4},{14341,2},{15414,40}}, +[65001]={{8170,30},{12803,4},{12810,3},{14341,1},{15414,30}}, +[65002]={{8170,30},{12803,4},{12810,2},{14341,2},{15414,25}}, +[65003]={{7971,4},{10285,8},{12662,20},{14048,12},{14256,20},{14341,1},{20520,10}}, +[65004]={{3577,6},{8846,10},{11382,2},{11752,1},{12360,14},{12644,4},{12938,1}}, +[65005]={{3860,24},{4304,4},{6037,10},{7910,8},{7966,6}}, +[65006]={{7082,12},{8170,30},{12810,16},{15407,4},{15415,40},{20295,1}}, +[65007]={{7910,1},{8170,4},{12359,24}}, +[65008]={{9197,20},{12360,14},{12364,10},{12644,4},{12803,10},{20002,10}}, +[65009]={{1210,6},{4236,4},{4304,8},{7428,8},{7971,2},{8343,2}}, +[65010]={{2840,8},{3470,2}}, +[65011]={{2840,8},{2880,4},{3470,4}}, +[65012]={{1705,2},{2841,6},{3391,2},{3466,2},{3478,4}}, +[65013]={{3466,4},{3486,4},{3829,2},{3859,10},{7070,4}}, +[65014]={{7076,6},{11371,10},{12360,10},{12809,8},{22203,4}}, +[65015]={{8170,24},{14341,1},{15407,1},{15415,25}}, +[65019]={{4304,30},{8165,25},{8172,3},{8343,4}}, +[65021]={{8211,1},{12803,20},{12810,16},{14227,4},{15407,4},{20002,10}}, +[65022]={{4304,40},{7075,12},{8172,3},{8343,4}}, +[65023]={{4304,20},{7081,10},{8172,1},{8343,4}}, +[65024]={{7076,20},{8170,20},{8343,2},{12809,10},{13455,5},{15407,6},{15419,20}}, +[65025]={{7078,25},{8170,20},{8343,2},{11751,2},{15407,5},{15417,15},{21546,5}}, +[65026]={{7080,20},{8170,15},{8343,2},{12457,10},{15407,5},{15422,20},{18294,10}}, +[65027]={{2459,10},{7082,20},{8170,10},{8343,2},{12753,6},{15407,6},{15423,20}}, +[65032]={{2931,2},{3372,1}}, +[65035]={{7078,10},{14227,4},{14342,6},{17010,5},{17011,4}}, +[65036]={{12607,6},{14227,4},{15407,5},{17010,5},{17011,3},{17012,6}}, +[65037]={{7076,6},{12607,6},{14227,4},{15407,4},{17010,3},{17011,6}}, +[65038]={{12607,6},{12810,12},{14227,4},{15407,5},{17010,9},{17012,12}}, +[65039]={{11371,14},{17010,6},{17011,5}}, +[81030]={{55156,1},{774,1},{55150,1}}, +[81031]={{2840,6},{774,2},{55245,1}}, +[81032]={{55150,1},{2770,1}}, +[81061]={{8170,6},{14047,4},{14341,1}}, +[81062]={{8170,12},{12803,1},{14341,1}}, +[81063]={{7080,1},{8170,10},{12803,1},{14341,1}}, +[81064]={{8170,12},{8343,2},{14341,1}}, +[81065]={{8170,8},{14047,4},{14341,1}}, +[81066]={{8170,24},{12803,4},{14341,1},{15407,1}}, +[81092]={{2840,4},{55245,1}}, +[81093]={{55156,1},{2880,1},{55245,1}}, +[83280]={{2324,1},{2842,1},{4339,4},{8343,2}}, +[83281]={{2324,1},{2842,2},{4339,10},{5500,1},{8343,4},{17028,1}}, +[83282]={{2324,1},{2842,1},{4339,4},{8343,1}}, +[83283]={{2324,1},{4304,2},{4339,3},{8343,3}}, +[83284]={{2324,1},{4339,3},{6048,1},{8343,2}}, +[83285]={{2324,1},{2842,1},{4339,6},{8343,2},{17028,1}}, +[83286]={{3827,2},{4339,3},{6260,1},{7070,1},{8343,1}}, +[83287]={{1705,2},{4339,6},{6260,2},{7070,2},{8343,1},{20746,1}}, +[83288]={{4234,3},{4339,2},{6260,1},{7070,1},{8343,1}}, +[83289]={{4339,3},{6260,1},{6373,1},{8343,2},{9036,1}}, +[83290]={{4339,3},{6149,2},{6260,1},{7070,1},{8343,1}}, +[83291]={{3827,2},{4339,4},{6260,1},{7070,1},{8343,1}}, +[83292]={{7068,2},{14048,5},{14341,1}}, +[83293]={{4625,1},{7068,1},{14048,4},{14341,3}}, +[83294]={{4625,4},{6037,2},{7068,3},{7078,2},{14048,8},{14341,4}}, +[83295]={{7077,4},{14048,2},{14341,1}}, +[83296]={{6371,1},{8170,2},{14048,2},{14341,1}}, +[83297]={{4625,1},{7077,4},{14048,4},{14341,2}}, +[83309]={{8838,1},{22529,1},{51714,2}}, +[83400]={{2605,1},{4291,2},{4304,8},{4338,4}}, +[83401]={{2605,2},{3575,2},{4291,4},{4304,12}}, +[83402]={{2605,1},{4234,2},{4291,3},{4304,10}}, +[83403]={{4234,10},{4236,1},{4291,1}}, +[83404]={{4291,2},{4304,6}}, +[83405]={{4291,3},{4304,7}}, +[83410]={{3859,14},{7966,2}}, +[83411]={{3859,16},{7966,4}}, +[83412]={{3859,18},{3864,1},{7966,2}}, +[83413]={{1705,1},{3859,20},{3864,1},{7966,4}}, +[83414]={{3859,20},{3864,1},{6037,1},{7966,3}}, +[83415]={{3859,10},{3864,4},{6037,8},{7909,2},{7922,1},{7966,3}}, +[84040]={{3713,1},{13464,1},{13889,1},{61173,1}}, +[84041]={{159,1},{12203,1},{12205,1}}, +} + +-- Per profession: { craftItemID, "cnName", "source" } +NanamiTradeSkillByProf = { +["Alchemy"]={ +{2454,"狮王之力药剂","A"}, +{118,"初级治疗药水","A"}, +{2455,"初级法力药水","T"}, +{2456,"初级活力药水","T"}, +{3390,"次级敏捷药剂","D"}, +{2458,"初级坚韧药剂","T"}, +{2459,"迅捷药水","D"}, +{858,"次级治疗药水","T"}, +{3382,"弱效巨魔之血药水","T"}, +{3383,"智慧药剂","T"}, +{3384,"初级抗魔药水","D"}, +{3385,"次级法力药水","T"}, +{3386,"抗毒药水","D"}, +{3387,"有限无敌药水","D"}, +{3388,"强力巨魔之血药水","T"}, +{3389,"防御药剂","T"}, +{3391,"食人魔力量药剂","D"}, +{2457,"初级敏捷药剂","D"}, +{929,"治疗药水","T"}, +{3823,"次级隐形药水","T"}, +{3824,"暗影之油","V"}, +{3825,"坚韧药剂","D"}, +{3826,"超强巨魔之血药水","D"}, +{3827,"法力药水","T"}, +{3828,"侦测次级隐形药剂","D"}, +{3829,"冰霜之油","V"}, +{4596,"透明治疗药水","Q"}, +{4623,"次级石盾药水","Q"}, +{5631,"怒气药水","V"}, +{5633,"暴怒药水","V"}, +{5634,"自由行动药剂","V"}, +{5996,"水下呼吸药剂","T"}, +{1710,"强效治疗药水","T"}, +{5997,"初级防御药剂","A"}, +{6051,"神圣防护药水","V"}, +{6048,"暗影防护药水","V"}, +{6049,"火焰防护药水","V"}, +{6050,"冰霜防护药水","V"}, +{6052,"自然防护药水","V"}, +{6370,"黑口鱼油","T"}, +{6371,"火焰之油","T"}, +{6372,"速游药水","T"}, +{6373,"火力药剂","T"}, +{6662,"增长药剂","D"}, +{6149,"强效法力药水","T"}, +{8949,"敏捷药剂","T"}, +{8951,"强效防御药剂","T"}, +{8956,"献祭之油","T"}, +{9030,"滋补药剂","Q"}, +{9036,"抗魔药水","D"}, +{9061,"地精火箭燃油","E"}, +{3928,"优质治疗药水","T"}, +{9144,"野葡萄药水","D"}, +{9149,"点金石","V"}, +{9154,"侦测亡灵药剂","T"}, +{9155,"奥法药剂","T"}, +{9172,"隐形药水","D"}, +{9179,"强效聪颖药剂","T"}, +{9088,"阿尔萨斯的礼物","D"}, +{9187,"强效敏捷药剂","T"}, +{9197,"梦境药剂","D"}, +{9206,"巨人药剂","D"}, +{9210,"幻象染料","V"}, +{9264,"暗影之力药剂","V"}, +{9224,"屠魔药剂","V"}, +{9233,"侦测恶魔药剂","T"}, +{3577,"点铁成金","V"}, +{6037,"转化秘银","V"}, +{10592,"猫眼药剂","T"}, +{12190,"昏睡药水","T"}, +{12360,"转化:奥金","V"}, +{13423,"石鳞鱼油","T"}, +{13442,"强效怒气药水","D"}, +{13443,"优质法力药水","V"}, +{13445,"超强防御药剂","V"}, +{13447,"先知药剂","D"}, +{13446,"特效治疗药水","V"}, +{13453,"蛮力药剂","Q"}, +{7078,"转化:点气成火","V"}, +{7076,"转化:点火成土","V"}, +{7080,"转化:转土成水","V"}, +{7082,"转化:点水成气","V"}, +{12808,"转化:水转死灵","D"}, +{12803,"转化:土转生命","D"}, +{13455,"强效石盾药水","D"}, +{13452,"猫鼬药剂","D"}, +{13462,"净化药水","D"}, +{13454,"强效奥法药剂","D"}, +{13457,"强效火焰防护药水","D"}, +{13456,"强效冰霜防护药水","D"}, +{13458,"强效自然防护药水","D"}, +{13461,"强效奥术防护药水","D"}, +{13459,"强效暗影防护药水","D"}, +{13444,"特效法力药水","D"}, +{13506,"化石合剂","D"}, +{13510,"泰坦合剂","D"}, +{13511,"精炼智慧合剂","D"}, +{13512,"超级能量合剂","D"}, +{13513,"多重抗性合剂","D"}, +{17708,"冰霜之力药剂","Q"}, +{18253,"特效活力药水","D"}, +{18294,"强效水下呼吸药剂","T"}, +{19931,"古拉巴什魔精","O"}, +{20007,"魔血药水","V"}, +{20002,"强效昏睡药水","V"}, +{20008,"活力行动药水","V"}, +{20004,"特效巨魔之血药水","V"}, +{7068,"转化:元素火焰","V"}, +{21546,"强效火力药剂","D"}, +{56113,"急速生长药剂","D"}, +{13460,"强效神圣防护药水","V"}, +{13503,"炼金石","D"}, +{17967,"精制奥妮克希亚鳞片","Q"}, +{55046,"强效冰霜之力药剂","V"}, +{55048,"强效奥术之力药剂","V"}, +{47410,"翡翠猫鼬药剂","Q"}, +{47412,"奥法巨人药剂","D"}, +{47414,"梦境火酒药剂","D"}, +{51262,"不稳定的混合物","?"}, +{50237,"强效自然力量药剂","D"}, +{61181,"加速药水","D"}, +{61225,"清醒药水","Q"}, +{61224,"梦境精华药剂","Q"}, +{7067,"转化:元素之土","V"}, +{7070,"转化:元素之水","V"}, +}, +["Blacksmithing"]={ +{2862,"劣质磨刀石","A"}, +{2851,"铜质链甲腰带","T"}, +{2852,"铜质链甲短裤","T"}, +{2853,"铜质护腕","A"}, +{2854,"铜质符文护腕","T"}, +{2863,"粗制磨刀石","T"}, +{2857,"铜质符文腰带","T"}, +{2864,"铜质符文胸甲","D"}, +{2865,"劣质青铜护腿","T"}, +{2866,"劣质青铜胸甲","T"}, +{2868,"青铜花纹护腕","T"}, +{2869,"镀银青铜胸甲","D"}, +{2871,"重磨刀石","T"}, +{2870,"银鳞胸甲","T"}, +{2844,"铜质钉锤","T"}, +{2845,"铜斧","T"}, +{2847,"铜质短剑","T"}, +{2848,"青铜钉锤","T"}, +{2849,"青铜斧","T"}, +{2850,"青铜短剑","T"}, +{3239,"劣质平衡石","A"}, +{3240,"粗制平衡石","T"}, +{3241,"重平衡石","T"}, +{3487,"铜质重剑","T"}, +{3488,"铜质战斧","T"}, +{3489,"厚重战斧","T"}, +{3490,"致命的青铜短剑","D"}, +{3491,"青铜重锤","T"}, +{3492,"巨型铁锤","D"}, +{3469,"铜质链甲战靴","T"}, +{3470,"劣质砂轮","T"}, +{3471,"铜质链甲外衣","D"}, +{3472,"铜质符文护手","T"}, +{3473,"铜质符文短裤","T"}, +{3474,"铜质宝石手套","D"}, +{3478,"粗制砂轮","T"}, +{3480,"劣质青铜护肩","T"}, +{3481,"镀银青铜护肩","D"}, +{3482,"镀银青铜战靴","T"}, +{3483,"镀银青铜护手","T"}, +{3484,"绿铁战靴","D"}, +{3485,"绿铁护手","D"}, +{3486,"重砂轮","T"}, +{3848,"青铜匕首","T"}, +{3849,"硬铁短剑","V"}, +{3850,"玉蛇刀","D"}, +{3851,"结实的铁锤","V"}, +{3852,"碎铁金锤","D"}, +{3853,"月钢宽剑","V"}, +{3854,"霜虎之刃","D"}, +{3855,"巨型铁斧","V"}, +{3856,"月牙斧","D"}, +{3835,"绿铁护腕","T"}, +{3836,"绿铁头盔","T"}, +{3837,"金鳞罩盔","V"}, +{3840,"绿铁护肩","D"}, +{3841,"金鳞护肩","D"}, +{3842,"绿铁护腿","T"}, +{3843,"金鳞护腿","D"}, +{3844,"绿铁锁甲","T"}, +{3845,"金鳞胸甲","D"}, +{3846,"精钢战靴","D"}, +{3847,"金鳞战靴","D"}, +{5540,"珍珠匕首","T"}, +{5541,"彩虹之锤","D"}, +{6042,"铁质盾刺","D"}, +{6043,"铁质平衡锤","D"}, +{6040,"金鳞护腕","T"}, +{6041,"钢质武器链","D"}, +{6214,"铜质大锤","T"}, +{6350,"劣质青铜战靴","T"}, +{6338,"银棒","T"}, +{6731,"铁炉堡胸甲","Q"}, +{7071,"铁扣环","T"}, +{7166,"铜质匕首","T"}, +{7913,"野人铁护肩","Q"}, +{7914,"野人铁质胸甲","Q"}, +{7915,"野人铁盔","Q"}, +{7916,"野人铁靴","Q"}, +{7917,"野人铁手套","Q"}, +{7963,"钢质胸甲","T"}, +{7964,"坚固的磨刀石","T"}, +{7966,"坚固的砂轮","T"}, +{7965,"坚固的平衡石","T"}, +{7918,"重型秘银护肩","T"}, +{7919,"重型秘银手套","T"}, +{7920,"秘银鳞片短裤","T"}, +{7921,"秘银短裤","D"}, +{7922,"钢质头盔","T"}, +{7924,"秘银鳞片护腕","V"}, +{7967,"秘银盾刺","D"}, +{7926,"精制秘银短裤","Q"}, +{7927,"精制秘银手套","Q"}, +{7928,"精制秘银护肩","Q"}, +{7938,"真银护手","T"}, +{7929,"兽人护腿","Q"}, +{7930,"重型秘银胸甲","T"}, +{7931,"秘银罩帽","T"}, +{7969,"秘银马刺","D"}, +{7932,"秘银鳞片护肩","D"}, +{7933,"秘银重靴","T"}, +{7934,"重型秘银头盔","D"}, +{7935,"精制秘银胸甲","Q"}, +{7939,"真银胸甲","T"}, +{7936,"精制秘银战靴","Q"}, +{7937,"精制秘银头盔","Q"}, +{7955,"铜质双刃刀","T"}, +{7956,"青铜战锤","T"}, +{7957,"青铜巨剑","T"}, +{7958,"青铜战斧","T"}, +{7941,"秘银重斧","T"}, +{7942,"蓝光斧","D"}, +{7943,"秘银魔剑","D"}, +{7945,"巨型黑色锤","T"}, +{7954,"粉碎者","T"}, +{7944,"秘银细剑","D"}, +{7961,"幻影之刃","T"}, +{7946,"秘银符文战锤","D"}, +{7959,"荒芜","T"}, +{7947,"乌木刀","V"}, +{7960,"真银圣剑","T"}, +{9060,"秘银杆","E"}, +{9366,"金鳞护手","Q"}, +{10423,"镀银青铜护腿","D"}, +{10421,"劣质铜外衣","A"}, +{11128,"金棒","T"}, +{11144,"真银棒","T"}, +{11608,"黑铁粉碎者","D"}, +{11606,"黑铁锁甲","D"}, +{11607,"黑铁斩碎者","D"}, +{11605,"黑铁护肩","D"}, +{11604,"黑铁板甲","D"}, +{12259,"亮闪闪的钢匕首","T"}, +{12260,"灼热金剑","D"}, +{12644,"致密砂轮","T"}, +{12643,"致密平衡石","T"}, +{12404,"致密磨刀石","T"}, +{12405,"瑟银护甲","D"}, +{12406,"瑟银腰带","D"}, +{12408,"瑟银护腕","D"}, +{12416,"辐光腰带","D"}, +{12428,"君王板甲护肩","Q"}, +{12424,"君王板甲腰带","Q"}, +{12415,"辐光胸甲","D"}, +{12425,"君王板甲护腕","Q"}, +{12624,"野刺锁甲","D"}, +{12645,"瑟银盾刺","D"}, +{12409,"瑟银长靴","D"}, +{12410,"瑟银头盔","D"}, +{12418,"辐光手套","D"}, +{12631,"炽热板甲护手","Q"}, +{12419,"辐光长靴","D"}, +{12426,"君王板甲战靴","Q"}, +{12427,"君王板甲头盔","Q"}, +{12417,"辐光头饰","D"}, +{12632,"风暴护手","D"}, +{12414,"瑟银护腿","D"}, +{12422,"君王板甲护胸","Q"}, +{12610,"符文板甲护肩","D"}, +{12611,"符文板甲战靴","D"}, +{12628,"魔铸胸甲","Q"}, +{12633,"白魂头盔","D"}, +{12420,"辐光护腿","D"}, +{12612,"符文板甲头盔","D"}, +{12636,"大酋长头盔","D"}, +{12640,"狮心头盔","D"}, +{12429,"君王板甲护腿","Q"}, +{12613,"符文板甲","D"}, +{12614,"符文板甲护腿","D"}, +{12639,"要塞护手","D"}, +{12620,"魔化瑟银头盔","Q"}, +{12619,"魔化瑟银护腿","Q"}, +{12618,"魔化瑟银胸甲","Q"}, +{12641,"免伤锁甲","D"}, +{12773,"华丽瑟银手斧","V"}, +{12774,"黎明之刃","Q"}, +{12775,"巨型瑟银战斧","V"}, +{12776,"魔化战锤","Q"}, +{12777,"闪耀轻剑","Q"}, +{12781,"平静","D"}, +{12792,"火山战锤","D"}, +{12782,"腐蚀术","D"}, +{12796,"泰坦之锤","D"}, +{12790,"奥金圣剑","D"}, +{12798,"歼灭者","D"}, +{12797,"寒冰护卫者","D"}, +{12794,"精工风暴战锤","D"}, +{12784,"奥金斧","D"}, +{12783,"寻心者","D"}, +{15869,"白银万能钥匙","T"}, +{15870,"黄金万能钥匙","T"}, +{15871,"真银万能钥匙","T"}, +{15872,"奥金万能钥匙","T"}, +{16206,"奥金棒","T"}, +{16989,"炽热链甲束带","V"}, +{16988,"炽热链甲护肩","V"}, +{17014,"黑铁护腕","V"}, +{17013,"黑铁护腿","V"}, +{17015,"黑铁利剑","V"}, +{17016,"黑铁战斧","V"}, +{17193,"萨弗隆战锤","Q"}, +{17704,"寒冬之刃","D"}, +{18262,"元素磨刀石","D"}, +{19043,"重型木喉腰带","V"}, +{19048,"重型木喉长靴","V"}, +{19051,"黎明束腰","V"}, +{19057,"黎明手套","V"}, +{19148,"黑铁头盔","V"}, +{19164,"黑铁护手","V"}, +{19166,"野蛮狂怒","V"}, +{19167,"黑色怒火","V"}, +{19170,"黑手","V"}, +{19168,"黑色卫士","V"}, +{19169,"夜幕","V"}, +{19690,"血魂胸甲","V"}, +{19691,"血魂护肩","V"}, +{19692,"血魂护手","V"}, +{19693,"黑暗之魂胸甲","V"}, +{19694,"黑暗之魂护腿","V"}, +{19695,"黑暗之魂护肩","V"}, +{20039,"黑铁长靴","V"}, +{20549,"黑暗符文护手","Q"}, +{20551,"黑暗符文头盔","Q"}, +{20550,"黑暗符文胸甲","Q"}, +{22197,"重型黑曜石腰带","V"}, +{22198,"碎裂黑曜石盾牌","V"}, +{22196,"厚重黑曜石胸甲","D"}, +{22195,"轻型黑曜石腰带","V"}, +{22194,"毁灭者的黑暗之握","D"}, +{22191,"黑曜石锁甲","V"}, +{22385,"泰坦护腿","D"}, +{22384,"说服者","D"}, +{22383,"先知之刃","D"}, +{22669,"破冰胸甲","T"}, +{22670,"破冰护手","T"}, +{22671,"破冰护腕","T"}, +{22762,"铁藤胸甲","V"}, +{22763,"铁藤手套","V"}, +{22764,"铁藤腰带","V"}, +{131,"钢带扣","D"}, +{66,"设计图:金带扣","D"}, +{67,"龙鳞带扣","D"}, +{82,"黑铁带扣","V"}, +{151,"真银带扣","D"}, +{87,"魔化瑟银带扣","D"}, +{103,"黑曜石带扣","D"}, +{2867,"劣质青铜护腕","V"}, +{6730,"铁炉堡链甲","V"}, +{6733,"铁炉堡护手","V"}, +{7925,"秘银鳞片手套","V"}, +{12625,"黎明使者护肩","D"}, +{12764,"瑟银巨剑","V"}, +{12769,"冷木斧","V"}, +{12772,"镶饰瑟银战锤","V"}, +{12779,"符文之刃","V"}, +{12795,"血爪","V"}, +{12802,"黑暗之矛","V"}, +{55058,"铭文板甲护腿","V"}, +{51264,"爆炸护盾","D"}, +{61364,"梦境钢铁护肩","D"}, +{61365,"梦境钢铁护腿","Q"}, +{61366,"梦境钢铁护腕","Q"}, +{61367,"梦境钢铁长靴","Q"}, +{61185,"黎明石锤","D"}, +{46600,"洛丹伦胸甲","Q"}, +{83410,"钢铁板甲长靴","T"}, +{83411,"钢铁板甲护手","T"}, +{83412,"钢铁板甲护腿","T"}, +{83413,"钢铁板甲胸甲","T"}, +{83414,"钢铁板甲护肩","T"}, +{83415,"钢铁板甲头盔","T"}, +{60294,"血石战刃","V"}, +{60293,"未淬火的符文剑","D"}, +{65004,"华丽血石匕首","Q"}, +{65005,"放血剃刀","?"}, +{65007,"帝国板甲护手","Q"}, +{65008,"梦境使者","V"}, +{65010,"铜指虎","T"}, +{65011,"锋利之爪","T"}, +{65012,"青铜荣耀者","T"}, +{65013,"冰霜缚斩者","D"}, +{65014,"偏斜护肩","D"}, +{60288,"符文蚀刻胫甲","D"}, +{60289,"符文蚀刻腿甲","D"}, +{60290,"符文蚀刻胸甲","D"}, +{60291,"符文蚀刻王冠","D"}, +{60292,"符文蚀刻护肩","D"}, +{60287,"符文蚀刻之握","D"}, +{60573,"仇恨熔炉头盔","D"}, +{60574,"仇恨熔炉胸甲","D"}, +{60575,"仇恨熔炉护腿","D"}, +{60576,"仇恨熔炉腰带","D"}, +{60577,"仇恨熔炉护手","D"}, +{60578,"仇恨熔炉靴子","D"}, +{61648,"木喉之怒","V"}, +{61649,"木喉肩铠","V"}, +{65039,"火链胸甲","V"}, +{60908,"半人马权威护肩","V"}, +{61182,"瑟银马刺","D"}, +{61779,"铜带扣","V"}, +{61780,"青铜带扣","V"}, +{61781,"铁扣环","V"}, +{61782,"秘银带扣","V"}, +{61783,"瑟银带扣","D"}, +{61784,"奥金带扣","D"}, +{61785,"梦钢带扣","Q"}, +{60007,"塔铸皇冠","D"}, +{60008,"塔铸胸甲","D"}, +{60009,"塔铸肩铠","D"}, +{60010,"塔铸破坏者","D"}, +{61810,"血腥带扣","Q"}, +{55526,"超凡罩帽","Q"}, +{55527,"超凡肩甲","Q"}, +{55528,"超凡胸甲","Q"}, +{55529,"超凡护腿","Q"}, +{55530,"炫光头盔","Q"}, +{55531,"炫光肩铠","Q"}, +{55532,"炫光胸甲","Q"}, +{55533,"炫光护腿","Q"}, +}, +["Cooking"]={ +{2679,"烧烤狼肉","A"}, +{2680,"香辣狼肉","T"}, +{2681,"烤野猪肉","A"}, +{2684,"山狗肉排","T"}, +{724,"猪肝馅饼","V"}, +{733,"杂味炖肉","V"}, +{2683,"蟹肉蛋糕","T"}, +{2682,"煮蟹爪","D"}, +{2687,"猪肉干","T"}, +{1082,"赤脊山炖肉","V"}, +{2685,"多汁猪排","D"}, +{1017,"干烤狼肉串","V"}, +{2888,"啤酒烤猪排","V"}, +{3662,"鳄鱼肉排","V"}, +{3220,"血肠","V"}, +{3663,"鱼人鳍汤","V"}, +{3664,"鳄鱼浓汤","V"}, +{3665,"美味煎蛋卷","V"}, +{3666,"蜘蛛蛋糕","V"}, +{3726,"大块的熊排","V"}, +{3727,"烤狮排","V"}, +{3728,"香烤狮肉","Q"}, +{3729,"海龟汤","Q"}, +{4457,"烧烤秃鹰翅膀","V"}, +{5472,"卡多雷蜘蛛烤肉","Q"}, +{5473,"蝎肉大餐","V"}, +{5474,"烤科多肉","V"}, +{5476,"狂鱼肉片","V"}, +{5477,"炖陆行鸟","V"}, +{5478,"掘地鼠炖肉","Q"}, +{5479,"脆炸蜥蜴尾","V"}, +{5480,"瘦鹿肉","V"}, +{5525,"水煮蚌肉","T"}, +{5527,"地精芥末蘸蚌肉","T"}, +{5526,"杂烩蚌肉","V"}, +{6038,"烧烤巨蚌","V"}, +{6290,"美味小鱼","V"}, +{787,"滑皮鲭鱼","V"}, +{4592,"长嘴泥鳅","V"}, +{6316,"洛克湖狂鱼","V"}, +{4593,"刺须鲶鱼","V"}, +{5095,"彩鳍鱼","V"}, +{4594,"石鳞鳕鱼","V"}, +{6657,"美味风蛇","D"}, +{6888,"草药烘蛋","A"}, +{6890,"熏熊肉","V"}, +{7676,"菊花茶","V"}, +{10841,"金棘茶","T"}, +{12209,"瘦狼排","V"}, +{12210,"烤迅猛龙肉","V"}, +{13851,"热狼排","V"}, +{12212,"丛林大杂烩","V"}, +{12213,"腐肉大餐","V"}, +{12214,"神秘杂烩","V"}, +{12217,"龙息红椒","V"}, +{12215,"科多肉杂烩","V"}, +{12216,"辣椒蟹肉","V"}, +{12218,"超级煎蛋卷","V"}, +{12224,"香脆蝙蝠翅","V"}, +{6887,"斑点黄尾鱼","V"}, +{13927,"煮熟的光滑大鱼","V"}, +{13928,"烤鱿鱼","V"}, +{13930,"油炸红腮鱼","V"}, +{13929,"烟熏鲈鱼","V"}, +{13931,"夜鳞鱼汤","V"}, +{13932,"水煮阳鳞鲑鱼","V"}, +{13933,"炖龙虾","V"}, +{13934,"大鱼片","V"}, +{13935,"烤鲑鱼","V"}, +{16766,"安德麦蚌肉杂烩","V"}, +{8364,"银头鲑鱼","V"}, +{17197,"姜饼","V"}, +{17198,"蛋奶酒","V"}, +{17222,"蜘蛛肉肠","T"}, +{18045,"嫩狼肉排","V"}, +{18254,"洛恩塔姆薯块","D"}, +{20074,"鳄鱼炖肉","V"}, +{20452,"沙漠肉丸子","Q"}, +{21023,"迪尔格的超美味奇美拉肉片","Q"}, +{21072,"烤鼠尾鱼","V"}, +{21217,"美味鼠尾鱼","V"}, +{30818,"海员香辣炖菜","Q"}, +{84040,"巧克力鱼","Q"}, +{84041,"吉尔尼斯热炖菜","V"}, +{53015,"古拉巴什浓汤","Q"}, +{83309,"强能草药沙拉","Q"}, +{60976,"达农佐的泰拉比姆惊喜","Q"}, +{60977,"达农佐的泰拉比姆趣味","Q"}, +{60978,"达农佐的泰拉比姆情调","Q"}, +{41674,"蜜汁烤猪排","V"}, +{41673,"克劳福德苹果挞","?"}, +}, +["Disguise"]={ +}, +["Enchanting"]={ +{6218,"符文铜棒","A"}, +{6339,"符文银棒","T"}, +{11130,"符文金棒","T"}, +{11145,"符文真银棒","T"}, +{11287,"次级魔法杖","T"}, +{11288,"强效魔法杖","T"}, +{11289,"次级秘法魔杖","T"}, +{11290,"强效秘法杖","T"}, +{11811,"浓烟山脉之心","D"}, +{12655,"魔化瑟银锭","T"}, +{12810,"魔化皮","T"}, +{16207,"符文奥金棒","V"}, +{20744,"初级巫师之油","V"}, +{20745,"初级法力之油","V"}, +{20746,"次级巫师之油","V"}, +{20747,"次级法力之油","V"}, +{20750,"巫师之油","V"}, +{20749,"卓越巫师之油","V"}, +{20748,"卓越法力之油","V"}, +{17968,"充能奥妮克希亚鳞片","Q"}, +{55248,"魔法宝石油","V"}, +{61732,"永恒梦境碎片","D"}, +}, +["Engineering"]={ +{4357,"劣质火药","A"}, +{4358,"劣质炸药","A"}, +{8067,"精制轻弹丸","A"}, +{4359,"一把螺栓","T"}, +{4360,"劣质铜壳炸弹","T"}, +{4361,"铜管","T"}, +{4362,"劣质火枪","T"}, +{4363,"铜质调节器","T"}, +{4401,"机械松鼠","D"}, +{4364,"粗制火药粉","T"}, +{8068,"精制重弹丸","T"}, +{4365,"粗制炸药","T"}, +{4366,"活动假人","T"}, +{4367,"小型爆盐炸弹","D"}, +{4368,"飞虎护目镜","T"}, +{4369,"致命的短枪","T"}, +{4370,"大型铜壳炸弹","T"}, +{4371,"青铜管","T"}, +{4372,"精致手工火枪","V"}, +{4373,"暗影护目镜","D"}, +{4374,"小型青铜炸弹","T"}, +{4375,"高速青铜齿轮","T"}, +{4376,"火焰偏斜器","D"}, +{4377,"烈性火药","T"}, +{4378,"烈性炸药","T"}, +{8069,"精制实心弹丸","T"}, +{4379,"镀银猎枪","T"}, +{4380,"重磅青铜炸弹","T"}, +{4381,"自动净化装置","V"}, +{4382,"青铜框架","T"}, +{4383,"夜视步枪","D"}, +{4384,"自爆绵羊","T"}, +{4385,"绿色护目镜","T"}, +{4386,"寒冰偏斜器","V"}, +{4387,"铁棒","T"}, +{4388,"退化射线","D"}, +{4403,"便携式青铜迫击炮","D"}, +{4389,"发条式同步协调陀螺仪","T"}, +{4390,"铁皮手雷","T"}, +{4391,"联合收割机组件","T"}, +{4392,"高级假人","T"}, +{4393,"工匠眼镜","D"}, +{4394,"重磅铁制炸弹","T"}, +{4395,"地精暗雷","D"}, +{4396,"机械幼龙","V"}, +{4397,"侏儒隐形装置","D"}, +{4398,"大型爆盐炸弹","D"}, +{4404,"银触媒","T"}, +{4405,"粗制瞄准镜","T"}, +{4406,"普通瞄准镜","T"}, +{4407,"精确瞄准镜","V"}, +{5507,"精制望远镜","T"}, +{6219,"扳手","T"}, +{4852,"闪光雷","Q"}, +{6712,"练习锁","T"}, +{6714,"简易投掷炸弹","D"}, +{7189,"地精火箭靴","T"}, +{7506,"侏儒通用遥控器","V"}, +{6533,"水下诱鱼器","T"}, +{7148,"地精起搏器","V"}, +{10558,"黄金能量核心","T"}, +{10505,"实心炸药","T"}, +{10507,"实心炸弹","T"}, +{10499,"增亮护目镜","D"}, +{10559,"秘银管","T"}, +{10498,"侏儒微调器","T"}, +{10560,"不牢固的扳机","T"}, +{10500,"火焰护目镜","T"}, +{10508,"秘银火枪","T"}, +{10512,"高速秘银弹头","T"}, +{10546,"致命瞄准镜","V"}, +{10561,"秘银外壳","T"}, +{10514,"秘银破片炸弹","T"}, +{10501,"猫眼超级护目镜","D"}, +{10510,"大口径秘银步枪","D"}, +{10502,"法术能量护目镜超级版","D"}, +{10518,"降落伞披风","D"}, +{10506,"潜水头盔","V"}, +{10503,"玫瑰色护目镜","T"}, +{10562,"高爆炸弹","T"}, +{10548,"狙击瞄准镜","D"}, +{10513,"秘银螺旋弹","T"}, +{10504,"绿色透镜","T"}, +{10576,"秘银机械幼龙","V"}, +{10644,"地精火箭燃油配方","T"}, +{10577,"地精迫击炮","T"}, +{10542,"地精采矿头盔","T"}, +{10543,"地精施工头盔","T"}, +{10586,"大炸弹","T"}, +{10587,"地精炸弹箱","T"}, +{10588,"地精火箭头盔","T"}, +{10645,"侏儒死亡射线","T"}, +{10646,"地精工兵炸药","T"}, +{10713,"秘银杆设计图","T"}, +{10545,"侏儒护目镜","T"}, +{10716,"侏儒缩小射线","T"}, +{10720,"侏儒撒网器","T"}, +{10721,"侏儒防护腰带","T"}, +{10724,"侏儒火箭靴","T"}, +{10725,"侏儒作战小鸡","T"}, +{10726,"侏儒洗脑帽","T"}, +{10727,"地精龙枪","T"}, +{11590,"机械修理包","T"}, +{11825,"炸弹宠物","G"}, +{11826,"发条娃娃","G"}, +{15846,"筛盐器","T"}, +{15992,"致密炸药粉","T"}, +{15993,"瑟银手榴弹","V"}, +{15994,"瑟银零件","V"}, +{15995,"瑟银火枪","D"}, +{15996,"仿真机械蛙","D"}, +{15999,"法术能量护目镜超级改良版","D"}, +{16000,"瑟银管","V"}, +{16004,"黑铁步枪","D"}, +{16005,"黑铁炸弹","D"}, +{15997,"瑟银弹","D"}, +{16023,"高级活动假人","V"}, +{16006,"精密奥金转换器","V"}, +{16009,"语音增强模组","D"}, +{16008,"高级技师护目镜","D"}, +{16022,"奥金幼龙","D"}, +{16040,"奥术炸弹","D"}, +{16007,"完美的奥金步枪","D"}, +{17716,"雪王9000型","Q"}, +{18232,"修理机器人74A型","O"}, +{18283,"比兹尼克247x128精确瞄准镜","D"}, +{18282,"火核狙击步枪","D"}, +{18168,"力反馈盾牌","D"}, +{9318,"红色焰火","V"}, +{9312,"蓝色焰火","V"}, +{9313,"绿色焰火","V"}, +{18588,"简易投掷炸弹II","V"}, +{18641,"致密炸药","T"}, +{18631,"真银变压器","V"}, +{18634,"寒冰偏斜器","V"}, +{18587,"地精起搏器XL型","D"}, +{18637,"强力净化器","D"}, +{18594,"强力爆盐炸弹","V"}, +{18638,"高辐射烈焰反射器","D"}, +{18639,"快速暗影反射器","D"}, +{18645,"报警机器人","D"}, +{18660,"世界放大器","D"}, +{18984,"空间撕裂器 - 永望镇","T"}, +{18986,"安全传送器 - 加基森","T"}, +{19026,"长蛇焰火","V"}, +{19999,"血藤护目镜","V"}, +{19998,"血藤透镜","V"}, +{21277,"安静的机械雪人","Q"}, +{21558,"小型蓝色烟花","Q"}, +{21559,"小型绿色烟花","Q"}, +{21557,"小型红色烟花","Q"}, +{21589,"大型蓝色烟花","Q"}, +{21590,"大型绿色烟花","Q"}, +{21592,"大型红色烟花","Q"}, +{21571,"蓝色烟花束","Q"}, +{21574,"绿色烟花束","Q"}, +{21576,"红色烟花束","Q"}, +{21714,"大型蓝色烟花束","Q"}, +{21716,"大型绿色烟花束","Q"}, +{21718,"大型红色烟花束","Q"}, +{21569,"烟花发射器","Q"}, +{21570,"烟花束发射器","Q"}, +{10585,"地精无线电","V"}, +{41326,"宝石透镜","V"}, +{41327,"宝石瞄准镜","D"}, +{41328,"精密珠宝工具包","D"}, +{51268,"不稳定的采矿炸药","D"}, +{61187,"精密陀螺仪护目镜","D"}, +{60098,"超能电池组","D"}, +{60099,"电动惩戒器","D"}, +{51312,"便携式虫洞发生器 - 暴风城","V"}, +{51313,"便携式虫洞发生器 - 奥格瑞玛","V"}, +{58304,"电压中和自然反射器","D"}, +{58305,"究极充能奥术反射器","D"}, +}, +["First Aid"]={ +{1251,"亚麻绷带","A"}, +{2581,"厚亚麻绷带","T"}, +{3530,"绒线绷带","T"}, +{3531,"厚绒线绷带","T"}, +{6450,"丝质绷带","T"}, +{6451,"厚丝质绷带","V"}, +{6452,"抗毒药剂","T"}, +{6453,"强力抗毒药剂","D"}, +{8544,"魔纹绷带","V"}, +{8545,"厚魔纹绷带","T"}, +{14529,"符文布绷带","T"}, +{14530,"厚符文布绷带","T"}, +{19440,"特效抗毒药剂","V"}, +{8546,"强力嗅盐","D"}, +}, +["Jewelcrafting"]={ +{156,"精致的矮人项链","D"}, +{56112,"古代矮人宝石","D"}, +{55150,"劣质砂纸","A"}, +{55156,"劣质的铜戒指","A"}, +{55157,"铜手镯","A"}, +{55060,"辛德拉长老的高贵法杖","V"}, +{55158,"明亮的铜戒指","T"}, +{55159,"坚硬的铜戒指","T"}, +{55160,"镶饰的铜戒指","T"}, +{81092,"铜杖","T"}, +{55161,"硬化铜手镯","T"}, +{55162,"小型强固戒指","T"}, +{55163,"虎纹戒指","T"}, +{55165,"小珍珠法杖","T"}, +{55166,"琥珀指环","T"}, +{55167,"碧蓝戒指","T"}, +{81031,"亮铜项链","T"}, +{55168,"柔光戒指","T"}, +{55170,"缀玉戒指","T"}, +{55151,"粗糙砂纸","T"}, +{81032,"劣质的宝石串","T"}, +{55171,"奢华宝石项链","T"}, +{55172,"琥珀石吊坠","T"}, +{55173,"深海项链","T"}, +{55174,"粗糙青铜戒指","T"}, +{41308,"闪亮青铜戒指","T"}, +{41309,"琥珀宝珠","T"}, +{55175,"硬化青铜法杖","T"}, +{55176,"岩石戒指","T"}, +{41310,"青铜手镯","T"}, +{41311,"暗影石戒指","T"}, +{41313,"青铜权杖","T"}, +{41312,"午夜吊坠","T"}, +{41314,"玛瑙头冠","T"}, +{41315,"月光法杖","T"}, +{41316,"禁锢徽记","T"}, +{41318,"魔化手镯","T"}, +{41320,"粗糙的宝石串","T"}, +{41319,"劣质瑟银戒指","T"}, +{41325,"银质勋章","T"}, +{41329,"纯银戒指","T"}, +{41332,"粗糙的铁戒指","T"}, +{41331,"粗糙的金戒指","T"}, +{41323,"灰烬石镶嵌指环","T"}, +{41321,"劣质瑟银戒指","T"}, +{41324,"秘银黑石项链","?"}, +{55154,"致密砂纸","T"}, +{55256,"明亮瑟银微光","?"}, +{55269,"铭文圣典","?"}, +{55271,"织法者之杖","?"}, +{55268,"水银环","?"}, +{55273,"水晶护腕","?"}, +{55267,"缥缈的霜花皇冠","?"}, +{41330,"奥术光辉吊坠","?"}, +{55152,"厚重砂纸","T"}, +{41344,"厚重的宝石串","T"}, +{55144,"金焰水晶手镯","T"}, +{55142,"石英指环","T"}, +{55148,"绽放翡翠之杖","T"}, +{55143,"翡翠和谐指环","T"}, +{55145,"黄金石英头冠","T"}, +{55146,"黄金酒杯","T"}, +{55147,"强能黄水晶吊坠","T"}, +{41322,"劣质秘银戒指","T"}, +{55141,"烈日黄玉戒指","T"}, +{41340,"闪光的金项链","T"}, +{41342,"铁花戒指","T"}, +{41343,"华丽的秘银权杖","T"}, +{55153,"坚固砂纸","T"}, +{55164,"小型巨魔血戒","D"}, +{41341,"劣质真银戒指","T"}, +{55196,"青绿石坠饰","T"}, +{56020,"精致的宝石串","T"}, +{41346,"强效禁锢徽记","T"}, +{41345,"皇家宝石法杖","T"}, +{41349,"灰烬石神像","T"}, +{41347,"符文真银戒指","T"}, +{55169,"小型珍珠戒指","D"}, +{81093,"笨重的铜戒指","D"}, +{55258,"天蓝星火","V"}, +{55265,"翡翠帝王石","D"}, +{55259,"光辉蓝宝石戒指","D"}, +{55272,"奥秘之杖","D"}, +{55266,"旭日皇冠","D"}, +{56023,"深海的凝视","D"}, +{55260,"星形瑟银指环","?"}, +{56032,"毁灭红玉指环","D"}, +{56031,"镶嵌宝石之戒","D"}, +{56033,"纯金戒指","?"}, +{55199,"棱光护符","D"}, +{55202,"黄玉坠饰","D"}, +{55197,"星形护符","D"}, +{55200,"虚空之心咒符","D"}, +{55204,"符文护符","D"}, +{55195,"星界护符","?"}, +{56034,"闪亮月亮石板","D"}, +{56035,"雷云魔印","D"}, +{55264,"巨型宝石指环","?"}, +{56036,"黄金权杖","?"}, +{55243,"宝石商的魔棒","?"}, +{55261,"星形红玉戒指","?"}, +{55178,"星型宝石护腕","?"}, +{55241,"深红护卫法杖","?"}, +{55198,"月光咒符","?"}, +{55263,"微光白玉坠戒","?"}, +{56037,"闪光项链","D"}, +{56038,"石之护符","D"}, +{56039,"火焰勋章","D"}, +{56040,"闪光的银项链","D"}, +{56041,"乌龟戒指","D"}, +{56042,"宝石项圈","D"}, +{56043,"金色徽记护符","V"}, +{56044,"闪亮护腕","D"}, +{56045,"曙光护腕","D"}, +{56046,"抑制头环","D"}, +{55180,"水晶火焰护腕","D"}, +{55228,"灰烬堕落戒指","D"}, +{55242,"柔光照明法杖","D"}, +{55255,"天火宝石戒指","V"}, +{55244,"宝石纲要","D"}, +{56048,"闪亮青绿石戒指","Q"}, +{56049,"迷人的黄玉项链","Q"}, +{56050,"精致的金手镯","Q"}, +{56051,"海洋之心","Q"}, +{56052,"加列拉之杖","Q"}, +{56053,"金玉戒指","D"}, +{56054,"精致秘银护符","Q"}, +{56055,"德莱尼水晶魔棒","Q"}, +{55316,"乌木戒指","D"}, +{55317,"王之决断","D"}, +{55318,"暗影宝珠","D"}, +{55319,"海洋之怒","V"}, +{55320,"闪耀月亮石指环","D"}, +{55321,"翼爪戒指","D"}, +{55322,"半人马指环","D"}, +{55323,"食人魔白骨指环","D"}, +{81030,"孔雀石戒指","T"}, +{55325,"水手的陨落","D"}, +{55326,"蛇盘之杖","D"}, +{55327,"法拉基祭祀图腾","D"}, +{55328,"古代法老智慧之杖","D"}, +{55329,"暗草护腕","D"}, +{56047,"水晶耳环","Q"}, +{55324,"幽灵戒指","D"}, +{56002,"尖锐黄水晶","Q"}, +{56004,"明亮灰烬石","Q"}, +{56006,"炽热红宝石","Q"}, +{56003,"闪亮水玉","D"}, +{56015,"艾泽拉斯红玉","D"}, +{56012,"幽影墨玉","D"}, +{56013,"漆黑曜石","D"}, +{56016,"奥术翡翠","D"}, +{56017,"猝火艾泽拉斯钻石","D"}, +{56014,"惊艳帝王石","D"}, +{56018,"魔化翡翠","D"}, +{56058,"无暇月亮石","V"}, +{56010,"绚丽宝钻","D"}, +{56000,"原始水晶石","T"}, +{56001,"圆润翡翠","T"}, +{56005,"幻光宝玉","T"}, +{56056,"燃星宝石","T"}, +{56008,"闪耀白玉","T"}, +{56009,"优雅翡翠","?"}, +{56007,"辉煌蓝宝石","?"}, +{56011,"不稳定的奥术宝石","?"}, +{56057,"辉光蓝宝石","?"}, +{56059,"闪烁的钻戒","D"}, +{56060,"熔火升华之冠","D"}, +{56061,"灰烬宝石护腕","D"}, +{56062,"支配之龙印","D"}, +{56063,"守御护符","D"}, +{56064,"精致钻石宝冠","Q"}, +{56065,"白玉指环","?"}, +{56066,"蓝玉指环","D"}, +{56067,"黑铁徽记之戒","V"}, +{56068,"白玉手镯","D"}, +{56069,"高雅之冠","D"}, +{56070,"华丽秘银手镯","Q"}, +{56071,"皇家曙光权杖","Q"}, +{56072,"幻彩吊坠","D"}, +{56073,"封印雕饰","D"}, +{55330,"刺花护腕","D"}, +{55331,"黑石铁夹","D"}, +{55332,"修道院灰烬护腕","D"}, +{55333,"影月宝珠","D"}, +{55334,"牙爪圣物","D"}, +{55335,"地狱灾祸手杖","D"}, +{55336,"海洋之根","D"}, +{55337,"雾林头冠","D"}, +{55338,"猛毒尖冠","?"}, +{55339,"血火头饰","?"}, +{55340,"影铸之眼","D"}, +{55341,"守护图腾","?"}, +{55210,"泛光月亮石胸针","D"}, +{55211,"黑曜石胸针","D"}, +{55212,"烟熏胸针","D"}, +{55213,"天青石胸针","D"}, +{56074,"璀璨玛瑙","D"}, +{56075,"深沉白玉","D"}, +{56077,"苏生黄玉","D"}, +{56076,"柔韧奥术石","D"}, +{56019,"致密的宝石串","T"}, +{56090,"织法者坠饰","D"}, +{56091,"午夜之戒","D"}, +{56092,"雷云护腕","T"}, +{56093,"雷云徽记","D"}, +{56094,"黄金符文戒指","D"}, +{56095,"魔法禁锢徽记","D"}, +{56089,"精致秘银头冠","D"}, +{55359,"光芒之怒指环","D"}, +{55360,"激发潜能之戒","D"}, +{55361,"支配之权杖","D"}, +{55362,"预视之眼","V"}, +{55363,"忘却的圣杯","D"}, +{55364,"破碎守护者的护符","D"}, +{55365,"粗野的聚焦手杖","D"}, +{55366,"魔导力量之杖","D"}, +{55367,"光亮护腕","D"}, +{55368,"辉煌女王的皇冠","D"}, +{56096,"精制钻石手镯","Q"}, +{61818,"华丽山地宝石","D"}, +}, +["Leatherworking"]={ +{2302,"手工皮靴","A"}, +{2304,"轻型护甲片","A"}, +{2303,"手工皮短裤","T"}, +{2307,"优质皮靴","D"}, +{2308,"优质皮披风","T"}, +{2300,"雕花皮外衣","T"}, +{2309,"雕花皮靴","T"}, +{2310,"雕花皮质披风","T"}, +{2311,"白色皮夹克","D"}, +{2312,"优质皮手套","D"}, +{2313,"中型护甲片","T"}, +{2314,"韧化皮甲","T"}, +{2315,"黑皮战靴","T"}, +{2316,"黑皮披风","T"}, +{2317,"黑皮外套","D"}, +{2318,"轻皮","A"}, +{4237,"手工皮带","T"}, +{4239,"雕花皮手套","T"}, +{4242,"雕花皮短裤","T"}, +{3719,"山地披风","T"}, +{4243,"优质皮外套","T"}, +{4244,"山地皮外衣","D"}, +{4246,"优质皮带","T"}, +{4247,"山地皮手套","T"}, +{4248,"黑皮手套","D"}, +{4249,"黑皮腰带","T"}, +{4250,"山地腰带","D"}, +{4251,"山地护肩","T"}, +{4252,"黑皮护肩","D"}, +{4253,"韧化皮手套","T"}, +{4254,"野人手套","D"}, +{4255,"绿色皮甲","V"}, +{4256,"守护之甲","D"}, +{4257,"绿色皮带","T"}, +{4258,"守护腰带","D"}, +{4259,"绿色皮护腕","T"}, +{4260,"守护腕甲","D"}, +{4262,"宝石皮带","V"}, +{4264,"野人腰带","D"}, +{4265,"重型护甲片","T"}, +{4231,"熟化轻毛皮","T"}, +{4233,"熟化中毛皮","T"}, +{4236,"熟化重毛皮","T"}, +{4455,"迅猛龙皮背心","V"}, +{4456,"迅猛龙皮腰带","V"}, +{5081,"科多兽皮包","Q"}, +{5739,"野人背心","T"}, +{5780,"鱼人鳞片腰带","D"}, +{5781,"鱼人鳞片胸甲","D"}, +{5782,"厚鱼人皮甲","D"}, +{5783,"鱼人鳞片护腕","D"}, +{5957,"手工皮外衣","A"}, +{5958,"优质皮裤","D"}, +{5961,"黑皮短裤","T"}, +{5962,"守护短裤","T"}, +{5963,"野人护腿","V"}, +{5964,"野人护肩","T"}, +{5965,"守护披风","D"}, +{5966,"守护手套","T"}, +{6466,"蛇鳞披风","V"}, +{6467,"蛇鳞手套","V"}, +{6468,"蛇鳞腰带","Q"}, +{6709,"月光外衣","Q"}, +{7276,"手工皮披风","A"}, +{7277,"手工皮护腕","A"}, +{7278,"轻皮箭袋","T"}, +{7279,"皮质小弹药包","T"}, +{7280,"皱褶皮短裤","D"}, +{7281,"轻皮护腕","T"}, +{7282,"轻皮短裤","T"}, +{7283,"黑色雏龙披风","V"}, +{7284,"红色雏龙手套","V"}, +{7285,"轻巧的皮手套","T"}, +{7348,"造弓师手套","T"}, +{7349,"采药人手套","V"}, +{7352,"土灵皮护肩","V"}, +{7358,"窃贼手套","D"}, +{7359,"重型土灵手套","D"}, +{7371,"重型箭袋","T"}, +{7372,"重皮弹药包","T"}, +{7373,"暗色皮护腿","D"}, +{7374,"暗色皮甲","T"}, +{7375,"绿色幼龙护甲","D"}, +{7377,"冰霜皮质披风","T"}, +{7378,"暗色护腕","T"}, +{7386,"绿色幼龙护腕","V"}, +{7387,"暗色皮带","T"}, +{7390,"暗色长靴","D"}, +{7391,"迅捷之靴","D"}, +{8172,"熟化厚毛皮","T"}, +{8173,"厚重护甲片","T"}, +{8174,"舒适的皮帽","D"}, +{8175,"夜色外套","T"}, +{8176,"夜色头带","T"}, +{8187,"龟壳手套","D"}, +{8189,"龟壳胸甲","T"}, +{8192,"夜色护肩","V"}, +{8198,"龟壳护腕","T"}, +{8200,"巫毒长袍","D"}, +{8203,"硬化蝎壳胸甲","D"}, +{8210,"蛮皮护肩","Q"}, +{8201,"巫毒面具","D"}, +{8205,"硬化蝎壳护腕","D"}, +{8204,"硬化蝎壳手套","D"}, +{8211,"蛮皮外衣","Q"}, +{8214,"蛮皮头盔","Q"}, +{8193,"夜色短裤","T"}, +{8191,"龟壳头盔","T"}, +{8209,"硬化蝎壳战靴","D"}, +{8185,"龟壳护腿","T"}, +{8197,"夜色长靴","T"}, +{8202,"巫毒短裤","D"}, +{8216,"巫毒披风","D"}, +{8207,"硬化蝎壳护肩","D"}, +{8213,"蛮皮战靴","Q"}, +{8206,"硬化蝎壳护腿","D"}, +{8208,"硬化蝎壳头盔","D"}, +{8212,"蛮皮护腿","Q"}, +{8215,"蛮皮披风","Q"}, +{8347,"龙鳞护手","T"}, +{8345,"狼头之盔","T"}, +{8346,"深海护手","T"}, +{8348,"火焰头盔","T"}, +{8349,"羽饰胸甲","T"}, +{8367,"龙鳞胸甲","T"}, +{8217,"快捷箭袋","T"}, +{8218,"厚皮弹药包","T"}, +{15407,"熟化毛皮","T"}, +{15077,"重型蝎壳护腕","V"}, +{15083,"邪恶皮甲护手","V"}, +{15045,"绿龙鳞片胸甲","V"}, +{15076,"重型蝎壳外衣","D"}, +{15084,"邪恶皮甲护腕","D"}, +{15074,"奇美拉手套","V"}, +{15047,"红龙鳞片胸甲","D"}, +{15091,"符文皮甲护手","D"}, +{15564,"毛皮护甲片","T"}, +{15054,"火山护腿","D"}, +{15046,"绿龙鳞片护腿","D"}, +{15061,"生命护肩","V"}, +{15067,"铁羽护肩","V"}, +{15073,"奇美拉长靴","D"}, +{15078,"重型蝎壳护手","D"}, +{15092,"符文皮甲护腕","D"}, +{15071,"霜刃长靴","V"}, +{15057,"雷暴短裤","V"}, +{15064,"战熊背心","D"}, +{15082,"重型蝎壳腰带","D"}, +{15086,"邪恶皮甲头环","D"}, +{15093,"符文皮甲腰带","D"}, +{15072,"奇美拉护腿","D"}, +{15069,"霜刃护腿","D"}, +{15079,"重型蝎壳护腿","D"}, +{15053,"火山胸甲","D"}, +{15048,"蓝龙鳞片胸甲","V"}, +{15060,"生命护腿","D"}, +{15056,"雷暴","D"}, +{15065,"战熊热裤","D"}, +{15075,"奇美拉外衣","D"}, +{15094,"符文皮甲头环","V"}, +{15087,"邪恶皮甲短裤","D"}, +{15063,"魔暴龙皮手套","V"}, +{15050,"黑色龙鳞胸甲","V"}, +{15066,"铁羽胸甲","D"}, +{15070,"霜刃手套","D"}, +{15080,"重型蝎壳头盔","V"}, +{15049,"蓝龙鳞片护肩","D"}, +{15058,"雷暴护肩","D"}, +{15095,"符文皮甲短裤","D"}, +{15088,"邪恶皮甲腰带","D"}, +{15138,"奥妮克希亚鳞片披风","Q"}, +{15051,"黑色龙鳞护肩","D"}, +{15059,"生命胸甲","D"}, +{15062,"魔暴龙皮护腿","D"}, +{15085,"邪恶皮甲","D"}, +{15081,"重型蝎壳护肩","D"}, +{15055,"火山护肩","D"}, +{15090,"符文皮甲","D"}, +{15096,"符文皮甲护肩","D"}, +{15068,"霜刃外套","D"}, +{15052,"黑色龙鳞护腿","D"}, +{2319,"中皮","T"}, +{4234,"重皮","T"}, +{4304,"厚皮","T"}, +{16982,"熔岩犬皮靴","V"}, +{16983,"熔铸头盔","V"}, +{16984,"黑色龙鳞战靴","V"}, +{17721,"冬天爷爷的手套","D"}, +{8170,"硬甲皮","T"}, +{18238,"影皮手套","V"}, +{18251,"熔火护甲片","D"}, +{18258,"戈多克食人魔装","Q"}, +{18504,"洞察束带","D"}, +{18506,"猫鼬长靴","D"}, +{18508,"迅行护腕","D"}, +{18509,"多彩披风","D"}, +{18510,"野性之皮","D"}, +{18511,"移形披风","D"}, +{18662,"重皮球","V"}, +{18948,"野人护腕","V"}, +{19044,"木喉之力","V"}, +{19049,"木喉作战手套","V"}, +{19052,"黎明皮靴","V"}, +{19058,"金色黎明衬肩","V"}, +{19149,"熔岩腰带","V"}, +{19157,"多彩护手","V"}, +{19162,"熔火犬皮腰带","V"}, +{19163,"熔火腰带","V"}, +{19685,"原始蝙蝠皮外套","V"}, +{19686,"原始蝙蝠皮手套","V"}, +{19687,"原始蝙蝠皮护腕","V"}, +{19688,"血虎胸甲","V"}, +{19689,"血虎护肩","V"}, +{20295,"蓝龙鳞片护腿","T"}, +{20296,"绿色龙鳞护手","T"}, +{20380,"梦幻龙鳞胸甲","V"}, +{20481,"飞火护腕","V"}, +{20480,"飞火护手","V"}, +{20479,"飞火胸甲","V"}, +{20476,"沙行者护腕","V"}, +{20477,"沙行者护手","V"}, +{20478,"沙行者胸甲","V"}, +{20575,"黑色雏龙外衣","V"}, +{21278,"雷暴手套","D"}, +{22661,"北极外套","T"}, +{22662,"北极手套","T"}, +{22663,"北极护腕","T"}, +{22664,"寒鳞胸甲","T"}, +{22666,"寒鳞护手","T"}, +{22665,"寒鳞护腕","T"}, +{22759,"荆木头盔","V"}, +{22760,"荆木长靴","V"}, +{22761,"荆木腰带","V"}, +{55043,"崇高领主甲胄","V"}, +{65,"龙喉护甲片","D"}, +{58112,"龙喉手套","D"}, +{8195,"夜色披风","V"}, +{15141,"奥妮克希亚鳞片胸甲","?"}, +{55050,"灌注精华手套","V"}, +{55054,"棱光鳞甲头盔","V"}, +{51284,"山猫步靴","D"}, +{61229,"梦境皮革","Q"}, +{61356,"梦境皮革披肩","D"}, +{61357,"梦境皮革护腕","Q"}, +{61358,"梦境皮革护腿","Q"}, +{61359,"梦境皮革腰带","Q"}, +{61188,"魔符雕刻护腕","D"}, +{83405,"骗子靴子","T"}, +{83404,"骗子手套","T"}, +{83403,"骗子腰带","T"}, +{83402,"骗子护腿","T"}, +{83401,"骗子外衣","T"}, +{83400,"骗子兜帽","T"}, +{65000,"红龙鳞片护腿","V"}, +{65001,"红龙鳞片护肩","D"}, +{65002,"红龙鳞片长靴","T"}, +{65006,"风暴鳞片护腿","D"}, +{65009,"暗影皮靴","D"}, +{65019,"龙鳞护腿","D"}, +{81061,"首领手套","T"}, +{81062,"首领护肩","T"}, +{81063,"首领头饰","T"}, +{81064,"首领短裤","T"}, +{81066,"首领马甲","T"}, +{81065,"首领长靴","T"}, +{65021,"翡翠梦境者护胸","V"}, +{65015,"蓝色龙鳞长靴","D"}, +{65038,"熔火猎犬手套","V"}, +{65036,"多彩护腿","V"}, +{65037,"熔岩护腿","V"}, +{65022,"大地胸甲","D"}, +{65023,"风之靴","D"}, +{65024,"大地卫士外套","D"}, +{65025,"火怒护腿","D"}, +{65026,"深渊猎手头盔","D"}, +{65027,"风行者之靴","D"}, +{60910,"半人马战甲","V"}, +{61183,"魔化护甲片","Q"}, +{55522,"虚灵头饰","Q"}, +{55523,"虚灵护肩","Q"}, +{55524,"虚灵外套","Q"}, +{55525,"虚灵护腿","Q"}, +}, +["Mining"]={ +{2840,"熔炼铜锭","A"}, +{2842,"熔炼银锭","T"}, +{2841,"熔炼青铜","T"}, +{3576,"熔炼锡锭","T"}, +{3575,"熔炼铁锭","T"}, +{3577,"熔炼金锭","V"}, +{3859,"熔炼钢锭","T"}, +{3860,"熔炼秘银","T"}, +{6037,"熔炼真银","V"}, +{11371,"熔炼黑铁","Q"}, +{12359,"熔炼瑟银","T"}, +{17771,"熔炼源质","T"}, +{61216,"熔炼梦境钢锭","Q"}, +}, +["Poisons"]={ +{2892,"致命毒药","T"}, +{2893,"致命毒药 II","T"}, +{3775,"致残毒药","T"}, +{3776,"致残毒药 II","T"}, +{5237,"麻痹毒药","T"}, +{5530,"致盲粉","T"}, +{6947,"速效毒药","A"}, +{6949,"速效毒药 II","T"}, +{6950,"速效毒药 III","T"}, +{6951,"麻痹毒药 II","T"}, +{8926,"速效毒药 IV","T"}, +{8927,"速效毒药 V","T"}, +{8928,"速效毒药 VI","T"}, +{8984,"致命毒药 III","T"}, +{8985,"致命毒药 IV","T"}, +{9186,"麻痹毒药 III","T"}, +{10918,"致伤毒药","T"}, +{10920,"致伤毒药 II","T"}, +{10921,"致伤毒药 III","T"}, +{10922,"致伤毒药 IV","T"}, +{20844,"致命毒药 V","D"}, +{65032,"煽动毒药I","T"}, +{54009,"溶解毒药","T"}, +{54010,"溶解毒药 II","T"}, +{47408,"腐蚀毒药","T"}, +{47409,"腐蚀毒药 II","D"}, +}, +["Tailoring"]={ +{2568,"棕色亚麻外衣","T"}, +{2569,"亚麻靴","T"}, +{2570,"亚麻披风","A"}, +{2572,"红色亚麻长袍","D"}, +{2575,"红色亚麻衬衣","T"}, +{2576,"白色亚麻衬衣","T"}, +{2577,"蓝色亚麻衬衣","T"}, +{2578,"野人亚麻外衣","T"}, +{2579,"绿色亚麻衬衣","T"}, +{2580,"强化亚麻斗篷","T"}, +{2582,"绿色毛纺外衣","T"}, +{2583,"毛纺靴","T"}, +{2584,"毛纺斗篷","T"}, +{2585,"灰色毛纺长袍","D"}, +{2587,"灰色毛纺衬衣","T"}, +{2996,"亚麻布卷","A"}, +{2997,"毛布卷","T"}, +{4238,"亚麻包","T"}, +{4240,"毛纺包","T"}, +{4241,"绿色毛纺包","D"}, +{4245,"丝绸小包","T"}, +{4305,"丝绸卷","T"}, +{4307,"高级亚麻手套","T"}, +{4308,"绿色亚麻护腕","T"}, +{4309,"手工亚麻裤","T"}, +{4310,"高级毛纺手套","T"}, +{4311,"高级毛纺披风","D"}, +{4312,"软底亚麻靴","T"}, +{4313,"红色毛纺靴","D"}, +{4314,"双线毛纺护肩","T"}, +{4315,"强化毛纺护肩","D"}, +{4316,"高级毛纺短裤","T"}, +{4317,"凤凰短裤","D"}, +{4318,"冥想手套","T"}, +{4319,"碧蓝丝质手套","V"}, +{4320,"蛛丝之靴","T"}, +{4321,"蛛丝便鞋","D"}, +{4322,"巫术师兜帽","V"}, +{4323,"暗影头巾","D"}, +{4324,"碧蓝丝质外衣","T"}, +{4325,"附魔师长靴","D"}, +{4326,"丝质长披风","T"}, +{4327,"冰覆披风","V"}, +{4328,"蜘蛛腰带","D"}, +{4329,"星辰腰带","D"}, +{4339,"魔纹布卷","T"}, +{4330,"漂亮的红衬衣","T"}, +{4331,"凤凰手套","D"}, +{4332,"淡黄色衬衣","V"}, +{4333,"黑丝衬衣","V"}, +{4334,"体面的白衬衣","T"}, +{4335,"紫色丝质衬衣","D"}, +{4336,"黑色冒险者衬衣","V"}, +{4343,"棕色亚麻短裤","T"}, +{4344,"棕色亚麻衬衣","A"}, +{5542,"珍珠披风","T"}, +{5762,"红色亚麻包","D"}, +{5763,"红色毛纺包","D"}, +{5766,"次级巫师袍","T"}, +{5770,"奥法之袍","D"}, +{5764,"绿色丝质包","D"}, +{5765,"黑色丝质背包","D"}, +{6238,"棕色亚麻长袍","T"}, +{6241,"白色亚麻长袍","T"}, +{6239,"红色亚麻外衣","D"}, +{6240,"蓝色亚麻外衣","V"}, +{6242,"蓝色亚麻长袍","V"}, +{6263,"蓝色罩衫","V"}, +{6264,"大师长袍","V"}, +{6384,"漂亮的蓝衬衣","D"}, +{6385,"漂亮的绿衬衣","D"}, +{6786,"简易的裙子","T"}, +{6787,"白色毛绒裙","T"}, +{6795,"白色冒险者衬衣","T"}, +{6796,"红色冒险者衬衣","T"}, +{7046,"碧蓝丝质短裤","T"}, +{7048,"碧蓝丝质头巾","T"}, +{7050,"丝质头带","T"}, +{7051,"土灵外衣","T"}, +{7052,"碧蓝丝质腰带","T"}, +{7054,"力量法袍","T"}, +{7055,"深红丝质腰带","T"}, +{7057,"绿色丝质护肩","T"}, +{7026,"亚麻腰带","T"}, +{7047,"黑暗之手","D"}, +{7049,"信念手套","D"}, +{7065,"绿色丝甲","D"}, +{7053,"碧蓝丝质披风","V"}, +{7056,"深红丝质披风","V"}, +{7058,"深红丝质外衣","T"}, +{7059,"深红丝质护肩","D"}, +{7060,"碧蓝护肩","D"}, +{7061,"土灵丝质腰带","D"}, +{7062,"深红丝质马裤","T"}, +{7063,"深红丝质长袍","V"}, +{7064,"深红丝质手套","T"}, +{10045,"简易亚麻短裤","A"}, +{10046,"简易的亚麻靴","T"}, +{10047,"简易的褶裙","T"}, +{10048,"多彩褶裙","D"}, +{9998,"黑色魔纹外衣","T"}, +{9999,"黑色魔纹短裤","T"}, +{10001,"黑色魔纹长袍","T"}, +{10002,"暗纹短裤","T"}, +{10003,"黑色魔纹手套","T"}, +{10004,"暗纹长袍","T"}, +{10007,"红色魔纹外衣","D"}, +{10008,"白色强盗面罩","D"}, +{10009,"红色魔纹短裤","D"}, +{10056,"橙色魔纹衬衣","T"}, +{10052,"橙色军用衬衣","V"}, +{10050,"魔纹包","T"}, +{10018,"红色魔纹手套","D"}, +{10019,"梦纹手套","T"}, +{10042,"灰布长袍","T"}, +{10021,"梦纹外衣","T"}, +{10023,"暗纹手套","T"}, +{10024,"黑色魔纹头带","T"}, +{10026,"黑色魔纹之靴","T"}, +{10027,"黑色魔纹护肩","T"}, +{10054,"紫色魔纹衬衣","V"}, +{10028,"暗纹护肩","T"}, +{10053,"简易的黑裙子","T"}, +{10029,"红色魔纹护肩","D"}, +{10051,"红色魔纹包","T"}, +{10055,"粉色魔纹衬衣","V"}, +{10030,"将军之帽","V"}, +{10031,"暗纹之靴","T"}, +{10033,"红色魔纹头带","D"}, +{10034,"礼服衬衣","V"}, +{10025,"暗纹面罩","Q"}, +{10044,"灰布长靴","T"}, +{10035,"礼服短裤","V"}, +{10040,"白色婚纱","V"}, +{10041,"梦纹头饰","T"}, +{10036,"礼服夹克","V"}, +{14048,"符文布卷","T"}, +{13856,"符文布腰带","T"}, +{13869,"霜纹外套","D"}, +{13868,"霜纹长袍","D"}, +{14046,"符文布背包","V"}, +{13858,"符文布袍","V"}, +{13857,"符文布外套","D"}, +{14042,"灰布外衣","D"}, +{13860,"符文布披风","V"}, +{14143,"鬼纹腰带","D"}, +{13870,"霜纹手套","D"}, +{14043,"灰布手套","D"}, +{14142,"鬼纹手套","D"}, +{14100,"亮布长袍","D"}, +{14101,"亮布手套","D"}, +{14141,"鬼纹外衣","D"}, +{13863,"符文布手套","V"}, +{14044,"灰布披风","D"}, +{14107,"恶魔布短裤","V"}, +{14103,"亮布披风","D"}, +{14132,"巫纹护腿","D"}, +{14134,"火焰披风","D"}, +{13864,"符文布靴","V"}, +{13871,"霜纹短裤","D"}, +{14045,"灰布短裤","D"}, +{14136,"冬夜法袍","D"}, +{14108,"恶魔布靴","D"}, +{13865,"符文布短裤","D"}, +{14104,"亮布短裤","D"}, +{14137,"月布护腿","D"}, +{14144,"鬼纹短裤","D"}, +{14111,"恶魔布帽","D"}, +{13866,"符文布头带","D"}, +{14155,"月布包","D"}, +{14128,"巫纹长袍","D"}, +{14138,"月布外衣","D"}, +{14139,"月布护肩","D"}, +{13867,"符文布护肩","D"}, +{14130,"巫纹头巾","D"}, +{14106,"恶魔布袍","D"}, +{14140,"月布头饰","D"}, +{14112,"恶魔布护肩","D"}, +{14146,"法术掌握手套","D"}, +{14156,"无底包","D"}, +{14154,"信念外衣","D"}, +{14152,"大法师之袍","D"}, +{14153,"虚空法袍","D"}, +{14342,"月布","V"}, +{15802,"月布长靴","Q"}, +{16980,"光芒衬肩","V"}, +{16979,"光芒手套","V"}, +{17723,"绿色节日衬衣","Q"}, +{18263,"光芒护腕","D"}, +{18258,"戈多克食人魔装","Q"}, +{18405,"大法师腰带","D"}, +{18407,"恶魔布手套","D"}, +{18408,"地狱火手套","D"}, +{18409,"月布手套","D"}, +{18413,"护卫披风","D"}, +{18486,"月布长袍","V"}, +{19047,"木喉之智","V"}, +{19050,"木喉衬肩","V"}, +{19056,"银色长靴","V"}, +{19059,"银色护肩","V"}, +{19156,"光芒长袍","V"}, +{19165,"光芒护腿","V"}, +{19682,"血藤外套","V"}, +{19683,"血藤护腿","V"}, +{19684,"血藤长靴","V"}, +{20538,"符文冥河护腿","D"}, +{20539,"符文冥河腰带","Q"}, +{20537,"符文冥河长靴","Q"}, +{21340,"灵魂袋","V"}, +{21341,"恶魔布包","O"}, +{21342,"熔火恶魔布包","D"}, +{21154,"红色节庆长裙","Q"}, +{21542,"红色节庆裤装","Q"}, +{22246,"魔化魔纹布包","V"}, +{22248,"魔化符文布包","V"}, +{22249,"大附魔袋","D"}, +{22251,"塞纳里奥草药包","V"}, +{22252,"塞纳留斯之袋","V"}, +{22654,"冰川手套","T"}, +{22652,"冰川外衣","T"}, +{22658,"冰川披风","T"}, +{22655,"冰川护腕","T"}, +{22660,"盖亚的拥抱","V"}, +{22756,"林栖者外衣","V"}, +{22757,"林栖者头冠","V"}, +{22758,"林栖者护肩","V"}, +{58134,"图样:暴掠手套","D"}, +{6243,"绿色毛纺长袍","V"}, +{7027,"黑暗之靴","V"}, +{10010,"雷织短裤","D"}, +{10011,"雷织手套","D"}, +{10020,"雷织外衣","D"}, +{10032,"雷织头带","D"}, +{10038,"雷织护肩","D"}, +{10039,"雷织长靴","D"}, +{55052,"观星者法袍","V"}, +{55056,"贵族编织斗篷","V"}, +{51256,"渴魔手套","D"}, +{61230,"梦境丝线","Q"}, +{61360,"梦境丝线披肩","D"}, +{61361,"梦境丝线裙裤","Q"}, +{61362,"梦境丝线护腕","Q"}, +{61363,"梦境丝线手套","Q"}, +{61186,"解谜手套","D"}, +{83280,"占卜者裤子","T"}, +{83281,"占卜者长袍","T"}, +{83282,"占卜者兜帽","T"}, +{83283,"占卜者鞋子","T"}, +{83284,"占卜者手套","T"}, +{83285,"占卜者护肩","T"}, +{83286,"预言家帽子","T"}, +{83287,"预言家长袍","T"}, +{83291,"预言家裤子","T"}, +{83290,"预言家护肩","T"}, +{83289,"预言家手套","T"}, +{83288,"预言家靴子","T"}, +{83292,"掠夺者兜帽","T"}, +{83293,"掠夺者披肩","T"}, +{83294,"掠夺者长袍","T"}, +{83295,"掠夺者手套","T"}, +{83296,"掠夺者鞋子","T"}, +{83297,"掠夺者裤子","T"}, +{65003,"牺牲长袍","D"}, +{65035,"烈焰核心靴子","V"}, +{60909,"导尘者腰带","V"}, +{60907,"缚风者手套","V"}, +{55518,"宇宙头饰","Q"}, +{55519,"宇宙衬肩","Q"}, +{55520,"宇宙外衣","Q"}, +{55521,"宇宙护腿","Q"}, +{55534,"魔网亲和披风","Q"}, +}, +} +end diff --git a/TradeSkillUI.lua b/TradeSkillUI.lua new file mode 100644 index 0000000..be4fa8b --- /dev/null +++ b/TradeSkillUI.lua @@ -0,0 +1,2130 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: TradeSkill / Craft UI (TradeSkillUI.lua) +-- Replaces TradeSkillFrame and CraftFrame with Nanami-UI styled interface +-- NOTE: Lua 5.0 upvalue limit = 32 per closure; all state packed into tables. +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.TradeSkillUI = {} +local TSUI = SFrames.TradeSkillUI +SFramesDB = SFramesDB or {} + +-------------------------------------------------------------------------------- +-- Theme (Pink Cat-Paw) +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + reagentOk = { 0.60, 0.90, 0.60 }, + reagentLack = { 0.90, 0.30, 0.30 }, + DIFFICULTY = { + optimal = { 1.00, 0.50, 0.25 }, + medium = { 1.00, 1.00, 0.00 }, + easy = { 0.25, 0.75, 0.25 }, + trivial = { 0.50, 0.50, 0.50 }, + difficult = { 1.00, 0.50, 0.25 }, + }, + QUALITY = { + [0] = { 0.62, 0.62, 0.62 }, + [1] = { 1.00, 1.00, 1.00 }, + [2] = { 0.12, 1.00, 0.00 }, + [3] = { 0.00, 0.44, 0.87 }, + [4] = { 0.64, 0.21, 0.93 }, + [5] = { 1.00, 0.50, 0.00 }, + }, +}) +T.DIFFICULTY.header = { T.catHeader[1], T.catHeader[2], T.catHeader[3] } + +-------------------------------------------------------------------------------- +-- Layout (packed into table to save upvalues) +-------------------------------------------------------------------------------- +local L = { + FRAME_W = 700, FRAME_H = 500, + HEADER_H = 46, SIDE_PAD = 10, + FILTER_H = 50, LIST_ROW_H = 26, + CAT_ROW_H = 20, BOTTOM_H = 40, + SCROLL_STEP = 40, SCROLLBAR_W = 12, + MAX_ROWS = 80, MAX_REAGENTS = 8, + LEFT_W = 304, +} +L.RIGHT_W = L.FRAME_W - L.LEFT_W +L.CONTENT_W = L.RIGHT_W - L.SIDE_PAD * 2 +L.LIST_ROW_W = L.LEFT_W - L.SIDE_PAD * 2 - L.SCROLLBAR_W - 4 + +-------------------------------------------------------------------------------- +-- State (packed into table to save upvalues) +-------------------------------------------------------------------------------- +local S = { + MainFrame = nil, + selectedIndex = nil, + currentFilter = "all", + searchText = "", + displayList = {}, + rowButtons = {}, + collapsedCats = {}, + craftAmount = 1, + currentMode = "tradeskill", + reagentSlots = {}, + profTabs = {}, + profList = {}, + switchStartTime = nil, +} + +-- Professions that can open a crafting window (spell name -> true) +local PROF_SPELLS = { + ["附魔"]=true,["Enchanting"]=true, + ["裁缝"]=true,["裁缝术"]=true,["Tailoring"]=true, + ["皮革加工"]=true,["皮革制作"]=true,["制皮"]=true,["Leatherworking"]=true, + ["锻造"]=true,["锻造术"]=true,["Blacksmithing"]=true, + ["工程学"]=true,["Engineering"]=true, + ["炼金术"]=true,["炼金"]=true,["Alchemy"]=true, + ["珠宝加工"]=true,["Jewelcrafting"]=true, + ["烹饪"]=true,["Cooking"]=true, + ["急救"]=true,["First Aid"]=true, + ["熔炼"]=true,["Smelting"]=true, + ["毒药"]=true,["Poisons"]=true, + ["训练野兽"]=true,["Beast Training"]=true, +} + +-------------------------------------------------------------------------------- +-- Mode Abstraction Layer (packed into table) +-------------------------------------------------------------------------------- +local API = {} + +function API.GetNumRecipes() + if S.currentMode == "craft" then + return GetNumCrafts and GetNumCrafts() or 0 + end + return GetNumTradeSkills and GetNumTradeSkills() or 0 +end + +function API.GetRecipeInfo(i) + if S.currentMode == "craft" then + if not GetCraftInfo then return nil end + local name, rank, skillType = GetCraftInfo(i) + return name, skillType, 0, false + end + if not GetTradeSkillInfo then return nil end + local name, skillType, numAvail, isExpanded = GetTradeSkillInfo(i) + return name, skillType, numAvail or 0, isExpanded +end + +function API.GetRecipeIcon(i) + if S.currentMode == "craft" then + return GetCraftIcon and GetCraftIcon(i) + end + return GetTradeSkillIcon and GetTradeSkillIcon(i) +end + +function API.GetSkillLineName() + if S.currentMode == "craft" then + if GetCraftDisplaySkillLine then + local name, cur, mx = GetCraftDisplaySkillLine() + if name and name ~= "" then + return name, cur, mx + end + end + local n = GetCraftName and GetCraftName() or "" + return n, 0, 0 + end + if GetTradeSkillLine then + local name, cur, mx = GetTradeSkillLine() + return name or "", cur or 0, mx or 0 + end + return "", 0, 0 +end + +function API.GetNumReagents(i) + if S.currentMode == "craft" then + return GetCraftNumReagents and GetCraftNumReagents(i) or 0 + end + return GetTradeSkillNumReagents and GetTradeSkillNumReagents(i) or 0 +end + +function API.GetReagentInfo(i, j) + if S.currentMode == "craft" then + if not GetCraftReagentInfo then return nil, nil, 0, 0 end + local name, tex, count, playerCount = GetCraftReagentInfo(i, j) + return name, tex, count or 0, playerCount or 0 + end + if not GetTradeSkillReagentInfo then return nil, nil, 0, 0 end + local name, tex, count, playerCount = GetTradeSkillReagentInfo(i, j) + return name, tex, count or 0, playerCount or 0 +end + +function API.DoRecipe(i, num) + if S.currentMode == "craft" then + if DoCraft then DoCraft(i) end + else + if DoTradeSkill then DoTradeSkill(i, num or 1) end + end +end + +function API.CloseRecipe() + if S.currentMode == "craft" then + if CloseCraft then pcall(CloseCraft) end + else + if CloseTradeSkill then pcall(CloseTradeSkill) end + end +end + +function API.SelectRecipe(i) + if S.currentMode == "craft" then + if SelectCraft then pcall(SelectCraft, i) end + else + if SelectTradeSkill then pcall(SelectTradeSkill, i) end + end +end + +function API.GetCooldown(i) + if S.currentMode == "craft" then return nil end + if not GetTradeSkillCooldown then return nil end + local ok, cd = pcall(GetTradeSkillCooldown, i) + if ok then return cd end + return nil +end + +function API.GetDescription(i) + if S.currentMode == "craft" then + if GetCraftDescription then + local ok, desc = pcall(GetCraftDescription, i) + if ok and desc then return desc end + end + end + return "" +end + +function API.GetNumMade(i) + if S.currentMode == "craft" then return 1, 1 end + if not GetTradeSkillNumMade then return 1, 1 end + local ok, lo, hi = pcall(GetTradeSkillNumMade, i) + if ok then return lo or 1, hi or 1 end + return 1, 1 +end + +function API.GetItemLink(i) + if S.currentMode == "craft" then + if GetCraftItemLink then + local ok, l = pcall(GetCraftItemLink, i); if ok then return l end + end + else + if GetTradeSkillItemLink then + local ok, l = pcall(GetTradeSkillItemLink, i); if ok then return l end + end + end + return nil +end + +function API.GetReagentItemLink(i, j) + if S.currentMode == "craft" then + if GetCraftReagentItemLink then + local ok, l = pcall(GetCraftReagentItemLink, i, j); if ok then return l end + end + else + if GetTradeSkillReagentItemLink then + local ok, l = pcall(GetTradeSkillReagentItemLink, i, j); if ok then return l end + end + end + return nil +end + +function API.GetItemQuality(i) + local link = API.GetItemLink(i) + if not link then return nil end + local _, _, colorHex = string.find(link, "|c(%x+)|") + if not colorHex then return nil end + local qualityMap = { + ["ff9d9d9d"] = 0, ["ffffffff"] = 1, + ["ff1eff00"] = 2, ["ff0070dd"] = 3, + ["ffa335ee"] = 4, ["ffff8000"] = 5, + } + return qualityMap[colorHex] +end + +function API.GetTools(i) + if S.currentMode == "craft" then + if GetCraftSpellFocus then + local ok, focus = pcall(GetCraftSpellFocus, i) + if ok and focus then return focus end + end + return nil + end + if not GetTradeSkillTools then return nil end + local ok, r1, r2, r3, r4, r5, r6, r7, r8, r9 = pcall(GetTradeSkillTools, i) + if not ok or not r1 then return nil end + -- GetTradeSkillTools may return two formats: + -- Standard: desc, tool1, has1, tool2, has2, ... + -- Alt (some servers): tool1, has1, tool2, has2, ... + -- Detect: if r2 is a string → standard (r1=desc); otherwise alt (r1=tool1) + local toolPairs = {} + if type(r2) == "string" then + if type(r2) == "string" then table.insert(toolPairs, {r2, r3}) end + if type(r4) == "string" then table.insert(toolPairs, {r4, r5}) end + if type(r6) == "string" then table.insert(toolPairs, {r6, r7}) end + if type(r8) == "string" then table.insert(toolPairs, {r8, r9}) end + else + if type(r1) == "string" then table.insert(toolPairs, {r1, r2}) end + if type(r3) == "string" then table.insert(toolPairs, {r3, r4}) end + if type(r5) == "string" then table.insert(toolPairs, {r5, r6}) end + if type(r7) == "string" then table.insert(toolPairs, {r7, r8}) end + end + if table.getn(toolPairs) == 0 then + if type(r1) == "string" then return r1 end + return nil + end + local parts = {} + for idx = 1, table.getn(toolPairs) do + local name = toolPairs[idx][1] + local has = toolPairs[idx][2] + if has then + table.insert(parts, "|cff60e060" .. name .. "|r") + else + table.insert(parts, "|cffff3030" .. name .. "|r") + end + end + return table.concat(parts, ", ") +end + +function API.GetCraftedItemID(i) + local link = API.GetItemLink(i) + if not link then return nil end + local _, _, id = string.find(link, "item:(%d+)") + if id then return tonumber(id) end + return nil +end + +function API.GetSkillThresholds(i) + if not NanamiTradeSkillDB then return nil end + local itemID = API.GetCraftedItemID(i) + if itemID and NanamiTradeSkillDB[itemID] then + return NanamiTradeSkillDB[itemID] + end + local name = API.GetRecipeInfo(i) + if name and NanamiTradeSkillDB[name] then + return NanamiTradeSkillDB[name] + end + return nil +end + +function API.FormatThresholds(thresholds) + if not thresholds then return nil end + return "|cffff7f3f" .. thresholds[1] .. "|r " + .. "|cffffff00" .. thresholds[2] .. "|r " + .. "|cff3fbf3f" .. thresholds[3] .. "|r " + .. "|cff7f7f7f" .. thresholds[4] .. "|r" +end + +local SOURCE_LABELS = { + T = { "|cffffffff训练师|r", "|cffffffff[T]|r" }, + A = { "|cffffffff自动学习|r", "|cffffffff[A]|r" }, + D = { "|cffa335ee掉落|r", "|cffa335ee[D]|r" }, + V = { "|cff1eff00商人|r", "|cff1eff00[V]|r" }, + Q = { "|cff0070dd任务|r", "|cff0070dd[Q]|r" }, + F = { "|cff40bfff钓鱼|r", "|cff40bfff[F]|r" }, + O = { "|cffffff00世界物体|r", "|cffffff00[O]|r" }, + E = { "|cffff8000工程制作|r", "|cffff8000[E]|r" }, + G = { "|cff888888赠予|r", "|cff888888[G]|r" }, + ["?"] = { "|cffffff00未知|r", "|cffffff00[?]|r" }, +} + +function API.GetRecipeSource(i) + if not NanamiTradeSkillSources then return nil end + local itemID = API.GetCraftedItemID(i) + if itemID and NanamiTradeSkillSources[itemID] then + return NanamiTradeSkillSources[itemID] + end + local name = API.GetRecipeInfo(i) + if name and NanamiTradeSkillSources[name] then + return NanamiTradeSkillSources[name] + end + return nil +end + +function API.ParseSource(src) + if not src then return nil, nil end + local stype = string.sub(src, 1, 1) + local detail = nil + if string.len(src) > 2 then + detail = string.sub(src, 3) + end + return stype, detail +end + +local PROF_CN_TO_EN = { + ["炼金术"] = "Alchemy", ["附魔"] = "Enchanting", ["锻造"] = "Blacksmithing", + ["制皮"] = "Leatherworking", ["裁缝"] = "Tailoring", ["工程学"] = "Engineering", + ["烹饪"] = "Cooking", ["急救"] = "First Aid", ["采矿"] = "Mining", + ["熔炼"] = "Smelting", ["草药学"] = "Herbalism", ["珠宝加工"] = "Jewelcrafting", + ["毒药"] = "Poisons", ["生存"] = "Survival", +} + +function API.GetUnlearnedRecipes() + if not NanamiTradeSkillByProf then return nil end + local skillName = API.GetSkillLineName() + if not skillName or skillName == "" then return nil end + local profKey = PROF_CN_TO_EN[skillName] or skillName + local allRecipes = NanamiTradeSkillByProf[profKey] + if not allRecipes then return nil end + + local knownItems = {} + local numRecipes = API.GetNumRecipes() + for i = 1, numRecipes do + local name, skillType = API.GetRecipeInfo(i) + if name and skillType ~= "header" then + local itemID = API.GetCraftedItemID(i) + if itemID then + knownItems[itemID] = true + end + end + end + + local unlearned = {} + for _, entry in ipairs(allRecipes) do + local craftItemID = entry[1] + local cnName = entry[2] + local src = entry[3] + if not knownItems[craftItemID] then + table.insert(unlearned, { + itemID = craftItemID, + name = cnName, + source = src, + }) + end + end + return unlearned +end + +-------------------------------------------------------------------------------- +-- Helpers (module methods to avoid extra upvalues) +-------------------------------------------------------------------------------- +function TSUI.GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +function TSUI.SetRoundBackdrop(frame) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) +end + +function TSUI.CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.45) + s:SetBackdropBorderColor(0, 0, 0, 0.6) + s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) + return s +end + +function TSUI.FormatCooldown(seconds) + if not seconds or seconds <= 0 then return nil end + if seconds >= 86400 then + return string.format("%.1f 天", seconds / 86400) + elseif seconds >= 3600 then + return string.format("%.1f 小时", seconds / 3600) + elseif seconds >= 60 then + return string.format("%d 分钟", math.floor(seconds / 60)) + else + return string.format("%d 秒", math.floor(seconds)) + end +end + +function TSUI.FormatRecipeForChat(recipeIndex) + local name = API.GetRecipeInfo(recipeIndex) + if not name then return nil end + local itemLink = API.GetItemLink(recipeIndex) + local header = itemLink or name + local numReagents = API.GetNumReagents(recipeIndex) + if numReagents == 0 then return header end + local parts = {} + for j = 1, numReagents do + local rName, _, rCount = API.GetReagentInfo(recipeIndex, j) + if rName then + local rLink = API.GetReagentItemLink(recipeIndex, j) + local display = rLink or rName + if rCount and rCount > 1 then + table.insert(parts, display .. "x" .. rCount) + else + table.insert(parts, display) + end + end + end + return header .. " = " .. table.concat(parts, ", ") +end + +function TSUI.LinkRecipeToChat(recipeIndex) + local msg = TSUI.FormatRecipeForChat(recipeIndex) + if not msg then return end + if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then + ChatFrameEditBox:Insert(msg) + else + if ChatFrame_OpenChat then + ChatFrame_OpenChat(msg) + elseif ChatFrameEditBox then + ChatFrameEditBox:Show() + ChatFrameEditBox:SetText(msg) + ChatFrameEditBox:SetFocus() + end + end +end + +local SOURCE_NAMES = { + T = "训练师", A = "自动学习", D = "掉落", V = "商人", + Q = "任务", F = "钓鱼", O = "世界物体", ["?"] = "未知", +} + +function TSUI.LinkUnlearnedToChat(data) + if not data or not data.name then return end + local msg = "[未学习] " .. data.name + local reagents = NanamiTradeSkillReagents and data.itemID and NanamiTradeSkillReagents[data.itemID] + if reagents and table.getn(reagents) > 0 then + local rParts = {} + for _, entry in ipairs(reagents) do + local rID, rCount = entry[1], entry[2] + local rName = GetItemInfo and GetItemInfo(rID) or ("#" .. rID) + if rCount > 1 then + table.insert(rParts, rName .. "x" .. rCount) + else + table.insert(rParts, rName) + end + end + msg = msg .. " = " .. table.concat(rParts, ", ") + end + if data.source then + local stype = API.ParseSource(data.source) + local sname = SOURCE_NAMES[stype] or stype + msg = msg .. " (来源: " .. sname .. ")" + end + if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then + ChatFrameEditBox:Insert(msg) + else + if ChatFrame_OpenChat then + ChatFrame_OpenChat(msg) + elseif ChatFrameEditBox then + ChatFrameEditBox:Show() + ChatFrameEditBox:SetText(msg) + ChatFrameEditBox:SetFocus() + end + end +end + +function TSUI.MatchSearch(name) + if not S.searchText or S.searchText == "" then return true end + if not name then return false end + local lowerSearch = string.lower(S.searchText) + local lowerName = string.lower(name) + return string.find(lowerName, lowerSearch, 1, true) ~= nil +end + +-------------------------------------------------------------------------------- +-- Widget Factories (module methods) +-------------------------------------------------------------------------------- +function TSUI.CreateFilterBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 60) + btn:SetHeight(20) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.5) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(TSUI.GetFont(), 11, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text) + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + btn.label = fs + + btn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + end) + btn:SetScript("OnLeave", function() + if this.active then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1) + else + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.5) + end + end) + + function btn:SetActive(flag) + self.active = flag + if flag then + self:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + self:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1) + self.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + else + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.5) + self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end + return btn +end + +function TSUI.CreateActionBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 100) + btn:SetHeight(28) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(TSUI.GetFont(), 12, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text) + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + btn.label = fs + btn.disabled = false + + function btn:SetDisabled(flag) + self.disabled = flag + if flag then + self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) + else + self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + end + end + + btn:SetScript("OnEnter", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end + end) + btn:SetScript("OnLeave", function() + if not this.disabled then + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end) + return btn +end + +function TSUI.CreateSpinner(parent, x, y) + local frame = CreateFrame("Frame", nil, parent) + frame:SetWidth(80); frame:SetHeight(24) + frame:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", x, y) + + local bg = CreateFrame("Frame", nil, frame) + bg:SetAllPoints() + bg:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) + bg:SetBackdropColor(T.searchBg[1], T.searchBg[2], T.searchBg[3], T.searchBg[4]) + bg:SetBackdropBorderColor(T.searchBorder[1], T.searchBorder[2], T.searchBorder[3], T.searchBorder[4]) + + local font = TSUI.GetFont() + local minus = CreateFrame("Button", nil, frame) + minus:SetWidth(20); minus:SetHeight(22); minus:SetPoint("LEFT", frame, "LEFT", 1, 0) + local minusFS = minus:CreateFontString(nil, "OVERLAY") + minusFS:SetFont(font, 14, "OUTLINE"); minusFS:SetPoint("CENTER", 0, 0) + minusFS:SetText("-"); minusFS:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + minus:SetScript("OnEnter", function() minusFS:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end) + minus:SetScript("OnLeave", function() minusFS:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end) + + local plus = CreateFrame("Button", nil, frame) + plus:SetWidth(20); plus:SetHeight(22); plus:SetPoint("RIGHT", frame, "RIGHT", -1, 0) + local plusFS = plus:CreateFontString(nil, "OVERLAY") + plusFS:SetFont(font, 14, "OUTLINE"); plusFS:SetPoint("CENTER", 0, 0) + plusFS:SetText("+"); plusFS:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + plus:SetScript("OnEnter", function() plusFS:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end) + plus:SetScript("OnLeave", function() plusFS:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end) + + local editBox = CreateFrame("EditBox", nil, frame) + editBox:SetWidth(36); editBox:SetHeight(20) + editBox:SetPoint("CENTER", 0, 0) + editBox:SetFont(font, 12, "OUTLINE") + editBox:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + editBox:SetJustifyH("CENTER") + editBox:SetAutoFocus(false) + editBox:SetNumeric(true) + editBox:SetMaxLetters(4) + editBox:SetText("1") + editBox:SetScript("OnEscapePressed", function() this:ClearFocus() end) + editBox:SetScript("OnEnterPressed", function() this:ClearFocus() end) + editBox:SetScript("OnEditFocusLost", function() + local v = tonumber(this:GetText()) or 1 + if v < 1 then v = 1 end + frame.value = v + this:SetText(tostring(v)) + end) + frame.editBox = editBox; frame.value = 1 + + function frame:SetValue(v) + if v < 1 then v = 1 end + self.value = v + self.editBox:SetText(tostring(v)) + end + function frame:GetValue() + local v = tonumber(self.editBox:GetText()) or self.value + if v < 1 then v = 1 end + return v + end + + minus:SetScript("OnClick", function() + local c = frame:GetValue(); frame:SetValue(IsShiftKeyDown() and (c - 10) or (c - 1)) + end) + plus:SetScript("OnClick", function() + local c = frame:GetValue(); frame:SetValue(IsShiftKeyDown() and (c + 10) or (c + 1)) + end) + return frame +end + +function TSUI.CreateListRow(parent, idx) + local row = CreateFrame("Button", nil, parent) + row:SetWidth(L.CONTENT_W); row:SetHeight(L.LIST_ROW_H) + + local iconFrame = CreateFrame("Frame", nil, row) + iconFrame:SetWidth(L.LIST_ROW_H - 4); iconFrame:SetHeight(L.LIST_ROW_H - 4) + iconFrame:SetPoint("LEFT", row, "LEFT", 0, 0) + iconFrame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 } }) + iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + row.iconFrame = iconFrame + + local icon = iconFrame:CreateTexture(nil, "ARTWORK") + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 3, -3) + icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -3, 3) + row.icon = icon + + local qualGlow = iconFrame:CreateTexture(nil, "OVERLAY") + qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + qualGlow:SetBlendMode("ADD"); qualGlow:SetAlpha(0.8) + local glowSize = (L.LIST_ROW_H - 4) * 1.9 + qualGlow:SetWidth(glowSize); qualGlow:SetHeight(glowSize) + qualGlow:SetPoint("CENTER", iconFrame, "CENTER", 0, 0) + qualGlow:Hide(); row.qualGlow = qualGlow + + local font = TSUI.GetFont() + local nameFS = row:CreateFontString(nil, "OVERLAY") + nameFS:SetFont(font, 11, "OUTLINE") + nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 6, 0) + nameFS:SetPoint("RIGHT", row, "RIGHT", -105, 0) + nameFS:SetJustifyH("LEFT"); nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + row.nameFS = nameFS + + local skillFS = row:CreateFontString(nil, "OVERLAY") + skillFS:SetFont(font, 8, "OUTLINE"); skillFS:SetPoint("RIGHT", row, "RIGHT", -46, 0) + skillFS:SetJustifyH("RIGHT") + skillFS:Hide(); row.skillFS = skillFS + + local srcTagFS = row:CreateFontString(nil, "OVERLAY") + srcTagFS:SetFont(font, 8, "OUTLINE"); srcTagFS:SetPoint("RIGHT", row, "RIGHT", -26, 0) + srcTagFS:SetJustifyH("RIGHT") + srcTagFS:Hide(); row.srcTagFS = srcTagFS + + local countFS = row:CreateFontString(nil, "OVERLAY") + countFS:SetFont(font, 10, "OUTLINE"); countFS:SetPoint("RIGHT", row, "RIGHT", -4, 0) + countFS:SetJustifyH("RIGHT"); countFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + countFS:Hide(); row.countFS = countFS + + local catFS = row:CreateFontString(nil, "OVERLAY") + catFS:SetFont(font, 11, "OUTLINE"); catFS:SetPoint("LEFT", row, "LEFT", 4, 0) + catFS:SetJustifyH("LEFT"); catFS:SetTextColor(T.catHeader[1], T.catHeader[2], T.catHeader[3]) + catFS:Hide(); row.catFS = catFS + + local catSep = row:CreateTexture(nil, "ARTWORK") + catSep:SetTexture("Interface\\Buttons\\WHITE8X8"); catSep:SetHeight(1) + catSep:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) + catSep:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0) + catSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) + catSep:Hide(); row.catSep = catSep + + local selBg = row:CreateTexture(nil, "ARTWORK") + selBg:SetTexture("Interface\\Buttons\\WHITE8X8") + selBg:SetAllPoints(row) + selBg:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.40) + selBg:Hide(); row.selBg = selBg + + local selGlow = row:CreateTexture(nil, "ARTWORK") + selGlow:SetTexture("Interface\\Buttons\\WHITE8X8") + selGlow:SetWidth(4); selGlow:SetHeight(L.LIST_ROW_H) + selGlow:SetPoint("LEFT", row, "LEFT", 0, 0) + selGlow:SetVertexColor(1, 0.65, 0.85, 1) + selGlow:Hide(); row.selGlow = selGlow + + local selTop = row:CreateTexture(nil, "OVERLAY") + selTop:SetTexture("Interface\\Buttons\\WHITE8X8") + selTop:SetHeight(1) + selTop:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0) + selTop:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 0) + selTop:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.8) + selTop:Hide(); row.selTop = selTop + + local selBot = row:CreateTexture(nil, "OVERLAY") + selBot:SetTexture("Interface\\Buttons\\WHITE8X8") + selBot:SetHeight(1) + selBot:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) + selBot:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0) + selBot:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.8) + selBot:Hide(); row.selBot = selBot + + local hl = row:CreateTexture(nil, "HIGHLIGHT") + hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + hl:SetBlendMode("ADD"); hl:SetAllPoints(row); hl:SetAlpha(0.3) + row.highlight = hl; row.recipeIndex = nil; row.isHeader = false; row.headerIndex = nil + + row:SetScript("OnEnter", function() + if this.recipeIndex and not this.isHeader then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + local ok + if S.currentMode == "craft" then + local link = GetCraftItemLink and GetCraftItemLink(this.recipeIndex) + if link then + ok = pcall(GameTooltip.SetCraftItem, GameTooltip, this.recipeIndex) + else + ok = pcall(GameTooltip.SetCraftSpell, GameTooltip, this.recipeIndex) + end + else + ok = pcall(GameTooltip.SetTradeSkillItem, GameTooltip, this.recipeIndex) + end + if ok then GameTooltip:Show() else GameTooltip:Hide() end + end + end) + row:SetScript("OnLeave", function() GameTooltip:Hide() end) + + function row:SetAsHeader(name, collapsed) + self.isHeader = true; self:SetHeight(L.CAT_ROW_H) + self.iconFrame:Hide(); self.nameFS:Hide(); self.countFS:Hide(); self.skillFS:Hide() + self.srcTagFS:Hide() + self.diffDot:Hide(); self.qualGlow:Hide(); self.highlight:SetAlpha(0.15) + self.selBg:Hide(); self.selGlow:Hide(); self.selTop:Hide(); self.selBot:Hide() + self.catFS:SetText((collapsed and "+" or "-") .. " " .. (name or "")) + self.catFS:Show(); self.catSep:Show() + end + + local diffDot = row:CreateTexture(nil, "OVERLAY") + diffDot:SetTexture("Interface\\Buttons\\WHITE8X8") + diffDot:SetWidth(4); diffDot:SetHeight(L.LIST_ROW_H - 8) + diffDot:SetPoint("LEFT", row, "LEFT", 0, 0) + row.diffDot = diffDot + + iconFrame:ClearAllPoints() + iconFrame:SetPoint("LEFT", row, "LEFT", 6, 0) + nameFS:ClearAllPoints() + nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 6, 0) + nameFS:SetPoint("RIGHT", row, "RIGHT", -105, 0) + + function row:SetAsRecipe(recipe) + self.isHeader = false; self.recipeIndex = recipe.index; self.unlearnedData = nil + self:SetHeight(L.LIST_ROW_H); self.iconFrame:Show(); self.nameFS:Show() + self.catFS:Hide(); self.catSep:Hide(); self.highlight:SetAlpha(0.3) + self.icon:SetTexture(API.GetRecipeIcon(recipe.index)) + self.nameFS:SetText(recipe.name) + local dc = T.DIFFICULTY[recipe.difficulty] or T.DIFFICULTY.trivial + self.nameFS:SetTextColor(dc[1], dc[2], dc[3]) + + self.diffDot:SetVertexColor(dc[1], dc[2], dc[3], 1) + self.diffDot:Show() + + local qc = recipe.quality and recipe.quality >= 2 and T.QUALITY[recipe.quality] + if qc then + self.qualGlow:SetVertexColor(qc[1], qc[2], qc[3]); self.qualGlow:Show() + else + self.qualGlow:Hide() + end + + if recipe.difficulty == "trivial" then + self.icon:SetVertexColor(0.6, 0.6, 0.6) + else + self.icon:SetVertexColor(1, 1, 1) + end + if recipe.numAvail and recipe.numAvail > 0 then + self.countFS:SetText("[" .. recipe.numAvail .. "]"); self.countFS:Show() + else self.countFS:Hide() end + + local thresholds = API.GetSkillThresholds(recipe.index) + if thresholds then + self.skillFS:SetText(API.FormatThresholds(thresholds)) + self.skillFS:Show() + else + self.skillFS:Hide() + end + + local src = API.GetRecipeSource(recipe.index) + if src then + local stype = API.ParseSource(src) + local lbl = SOURCE_LABELS[stype] + if lbl then + self.srcTagFS:SetText(lbl[2]) + self.srcTagFS:Show() + else + self.srcTagFS:Hide() + end + else + self.srcTagFS:Hide() + end + end + + function row:SetAsUnlearned(data) + self.isHeader = false; self.recipeIndex = nil + self.unlearnedData = data + self:SetHeight(L.LIST_ROW_H); self.iconFrame:Show(); self.nameFS:Show() + self.catFS:Hide(); self.catSep:Hide(); self.highlight:SetAlpha(0.15) + self.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") + self.icon:SetVertexColor(0.5, 0.5, 0.5) + self.nameFS:SetText("|cff888888" .. (data.name or "?") .. "|r") + self.nameFS:SetTextColor(0.55, 0.55, 0.55) + self.diffDot:SetVertexColor(0.4, 0.4, 0.4, 1); self.diffDot:Show() + self.qualGlow:Hide(); self.countFS:Hide() + self.selBg:Hide(); self.selGlow:Hide(); self.selTop:Hide(); self.selBot:Hide() + + if data.thresholds then + self.skillFS:SetText(API.FormatThresholds(data.thresholds)) + self.skillFS:Show() + else + self.skillFS:Hide() + end + + local stype = data.sourceType + local lbl = SOURCE_LABELS[stype] + if lbl then + self.srcTagFS:SetText(lbl[2]) + self.srcTagFS:Show() + else + self.srcTagFS:Hide() + end + end + + function row:Clear() + self.recipeIndex = nil; self.headerIndex = nil; self.isHeader = false + self.unlearnedData = nil + self.countFS:Hide(); self.diffDot:Hide(); self.qualGlow:Hide(); self.skillFS:Hide() + self.srcTagFS:Hide() + self.selBg:Hide(); self.selGlow:Hide(); self.selTop:Hide(); self.selBot:Hide() + self:Hide() + end + return row +end + +function TSUI.CreateReagentSlot(parent, i) + local slot = CreateFrame("Frame", nil, parent) + slot:SetWidth(L.CONTENT_W / 2 - 4); slot:SetHeight(30) + + local rIconFrame = CreateFrame("Frame", nil, slot) + rIconFrame:SetWidth(28); rIconFrame:SetHeight(28); rIconFrame:SetPoint("LEFT", slot, "LEFT", 0, 0) + rIconFrame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 } }) + rIconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + rIconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + slot.iconFrame = rIconFrame + + local rIcon = rIconFrame:CreateTexture(nil, "ARTWORK") + rIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + rIcon:SetPoint("TOPLEFT", rIconFrame, "TOPLEFT", 3, -3) + rIcon:SetPoint("BOTTOMRIGHT", rIconFrame, "BOTTOMRIGHT", -3, 3) + slot.icon = rIcon + + local font = TSUI.GetFont() + local rNameFS = slot:CreateFontString(nil, "OVERLAY") + rNameFS:SetFont(font, 11, "OUTLINE"); rNameFS:SetPoint("LEFT", rIconFrame, "RIGHT", 6, 0) + rNameFS:SetPoint("RIGHT", slot, "RIGHT", -46, 0); rNameFS:SetJustifyH("LEFT") + rNameFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]); slot.nameFS = rNameFS + + local rCountFS = slot:CreateFontString(nil, "OVERLAY") + rCountFS:SetFont(font, 11, "OUTLINE"); rCountFS:SetPoint("RIGHT", slot, "RIGHT", -2, 0) + rCountFS:SetJustifyH("RIGHT"); slot.countFS = rCountFS + + slot.reagentIndex = nil; slot:EnableMouse(true) + slot:SetScript("OnEnter", function() + if S.selectedIndex and this.reagentIndex then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + local ok + if S.currentMode == "craft" then + local link = GetCraftItemLink and GetCraftItemLink(S.selectedIndex) + if link then + ok = pcall(GameTooltip.SetCraftItem, GameTooltip, S.selectedIndex, this.reagentIndex) + else + ok = pcall(GameTooltip.SetCraftSpell, GameTooltip, S.selectedIndex) + end + else + ok = pcall(GameTooltip.SetTradeSkillItem, GameTooltip, S.selectedIndex, this.reagentIndex) + end + if ok then GameTooltip:Show() else GameTooltip:Hide() end + end + end) + slot:SetScript("OnLeave", function() GameTooltip:Hide() end) + slot:SetScript("OnMouseUp", function() + if IsShiftKeyDown() and S.selectedIndex and this.reagentIndex then + local link = API.GetReagentItemLink(S.selectedIndex, this.reagentIndex) + if link then + if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then + ChatFrameEditBox:Insert(link) + else + if ChatFrame_OpenChat then + ChatFrame_OpenChat(link) + elseif ChatFrameEditBox then + ChatFrameEditBox:Show() + ChatFrameEditBox:SetText(link) + ChatFrameEditBox:SetFocus() + end + end + end + end + end) + slot:Hide() + return slot +end + +-------------------------------------------------------------------------------- +-- Logic (module methods referencing S, L, T, API only) +-------------------------------------------------------------------------------- +function TSUI.BuildDisplayList() + S.displayList = {} + + if S.currentFilter == "unlearned" then + local unlearned = API.GetUnlearnedRecipes() + if not unlearned or table.getn(unlearned) == 0 then return end + local filtered = {} + for _, entry in ipairs(unlearned) do + if TSUI.MatchSearch(entry.name) then + local stype = API.ParseSource(entry.source or "T") + local th = NanamiTradeSkillDB and NanamiTradeSkillDB[entry.itemID] + table.insert(filtered, { + type = "unlearned", + data = { + name = entry.name, + itemID = entry.itemID, + source = entry.source, + sourceType = stype, + thresholds = th, + }, + }) + end + end + if table.getn(filtered) == 0 then return end + table.insert(S.displayList, { type = "header", name = "未学习的配方 (" .. table.getn(filtered) .. ")", collapsed = false, headerIndex = 0 }) + for _, item in ipairs(filtered) do + table.insert(S.displayList, item) + end + return + end + + local numRecipes = API.GetNumRecipes() + if numRecipes == 0 then return end + + local currentCat = nil + local catRecipes = {} + local catOrder = {} + + for i = 1, numRecipes do + local name, skillType, numAvail, isExpanded = API.GetRecipeInfo(i) + if name then + if skillType == "header" then + currentCat = name + if not catRecipes[name] then + catRecipes[name] = {} + table.insert(catOrder, { name = name, index = i }) + end + else + if not currentCat then + currentCat = "配方" + if not catRecipes[currentCat] then + catRecipes[currentCat] = {} + table.insert(catOrder, { name = currentCat, index = 0 }) + end + end + local show = true + if S.currentFilter == "available" then + show = (skillType ~= "trivial" and numAvail > 0) + elseif S.currentFilter == "optimal" then + show = (skillType == "optimal" or skillType == "difficult" or skillType == "medium") + elseif S.currentFilter == "hasmat" then + show = (numAvail and numAvail > 0) + end + if show and not TSUI.MatchSearch(name) then show = false end + if show then + local quality = API.GetItemQuality(i) + table.insert(catRecipes[currentCat], { + index = i, name = name, + difficulty = skillType or "trivial", + numAvail = numAvail or 0, + quality = quality, + }) + end + end + end + end + + local hasCats = table.getn(catOrder) > 1 + for _, catInfo in ipairs(catOrder) do + local catName = catInfo.name + local recipes = catRecipes[catName] + if recipes and table.getn(recipes) > 0 then + if hasCats then + table.insert(S.displayList, { type = "header", name = catName, + collapsed = S.collapsedCats[catName], headerIndex = catInfo.index }) + end + if not S.collapsedCats[catName] then + for _, recipe in ipairs(recipes) do + table.insert(S.displayList, { type = "recipe", data = recipe }) + end + end + end + end +end + +function TSUI.UpdateList() + if not S.MainFrame or not S.MainFrame:IsVisible() then return end + TSUI.BuildDisplayList() + local content = S.MainFrame.listScroll.content + local count = table.getn(S.displayList) + local y = 0 + for i = 1, L.MAX_ROWS do + local row = S.rowButtons[i] + if i <= count then + local entry = S.displayList[i] + row:ClearAllPoints() + if entry.type == "header" then + row:SetAsHeader(entry.name, entry.collapsed) + row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + row.catName = entry.name; row.headerIndex = entry.headerIndex + row:Show(); y = y + L.CAT_ROW_H + elseif entry.type == "unlearned" then + row:SetAsUnlearned(entry.data) + row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + row.catName = nil; row:Show(); y = y + L.LIST_ROW_H + if S.selectedUnlearned and S.selectedUnlearned.itemID == entry.data.itemID then + row.selBg:Show(); row.selGlow:Show(); row.selTop:Show(); row.selBot:Show() + end + else + row:SetAsRecipe(entry.data) + row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + row.catName = nil; row:Show(); y = y + L.LIST_ROW_H + if S.selectedIndex == entry.data.index then + row.iconFrame:SetBackdropBorderColor(1, 0.65, 0.85, 1) + row.iconFrame:SetBackdropColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.5) + row.selBg:Show() + row.selGlow:Show() + row.selTop:Show() + row.selBot:Show() + row.nameFS:SetTextColor(1, 1, 1) + else + row.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + row.iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + row.selBg:Hide() + row.selGlow:Hide() + row.selTop:Hide() + row.selBot:Hide() + end + end + else row:Clear() end + end + content:SetHeight(math.max(1, y)) +end + +function TSUI.UpdateDetail() + if not S.MainFrame then return end + local detail = S.MainFrame.detail + if not S.selectedIndex then + if S.selectedUnlearned then + TSUI.UpdateDetailUnlearned(S.selectedUnlearned) + return + end + detail.iconFrame:Hide(); detail.qualGlow:Hide() + detail.nameFS:SetText(""); detail.descFS:SetText("") + detail.cooldownFS:SetText(""); detail.toolsFS:SetText(""); detail.madeFS:SetText("") + if detail.diffDot then detail.diffDot:Hide() end + if detail.diffFS then detail.diffFS:SetText("") end + if detail.sourceFS then detail.sourceFS:SetText("") end + for i = 1, L.MAX_REAGENTS do S.reagentSlots[i]:Hide() end + S.MainFrame.createBtn:SetDisabled(true); S.MainFrame.createAllBtn:SetDisabled(true) + return + end + local name, skillType, numAvail = API.GetRecipeInfo(S.selectedIndex) + detail.icon:SetTexture(API.GetRecipeIcon(S.selectedIndex)); detail.iconFrame:Show() + detail.nameFS:SetText(name or "") + local dc = T.DIFFICULTY[skillType] or T.DIFFICULTY.trivial + detail.nameFS:SetTextColor(dc[1], dc[2], dc[3]) + + local quality = API.GetItemQuality(S.selectedIndex) + local qc = quality and quality >= 2 and T.QUALITY[quality] + if qc then + detail.qualGlow:SetVertexColor(qc[1], qc[2], qc[3]); detail.qualGlow:Show() + else + detail.qualGlow:Hide() + end + + local cdText = TSUI.FormatCooldown(API.GetCooldown(S.selectedIndex)) + detail.cooldownFS:SetText(cdText and ("|cffff8040冷却: " .. cdText .. "|r") or "") + + local tools = API.GetTools(S.selectedIndex) + detail.toolsFS:SetText(tools and ("|cffffcc80需要: |r" .. tools) or "") + detail.descFS:SetText(API.GetDescription(S.selectedIndex) or "") + + local loMade, hiMade = API.GetNumMade(S.selectedIndex) + if loMade and hiMade and (loMade > 1 or hiMade > 1) then + local mt = (loMade == hiMade) and ("产出: " .. loMade) or ("产出: " .. loMade .. "-" .. hiMade) + detail.madeFS:SetText("|cffa0a0a0" .. mt .. "|r") + else detail.madeFS:SetText("") end + + if detail.diffDot then + local DIFF_NAMES = { + optimal = { "橙色", "必涨点" }, + difficult = { "橙色", "必涨点" }, + medium = { "黄色", "大概率涨点" }, + easy = { "绿色", "小概率涨点" }, + trivial = { "灰色", "不涨点" }, + } + local info = DIFF_NAMES[skillType] + local thresholds = API.GetSkillThresholds(S.selectedIndex) + + if info then + detail.diffDot:SetVertexColor(dc[1], dc[2], dc[3], 1) + detail.diffDot:Show() + if thresholds then + local thText = API.FormatThresholds(thresholds) + detail.diffFS:SetText(info[2] .. " |cffbbbbbb技能:|r " .. thText) + else + detail.diffFS:SetText("当前难度: " .. info[1] .. " (" .. info[2] .. ")") + end + detail.diffFS:SetTextColor(dc[1], dc[2], dc[3]) + else + detail.diffDot:Hide() + detail.diffFS:SetText("") + end + end + + if detail.sourceFS then + local src = API.GetRecipeSource(S.selectedIndex) + if src then + local stype, sdetail = API.ParseSource(src) + local lbl = SOURCE_LABELS[stype] + if lbl then + local text = "|cffbbbbbb来源:|r " .. lbl[1] + if sdetail and sdetail ~= "" then + text = text .. " - " .. sdetail + end + detail.sourceFS:SetText(text) + else + detail.sourceFS:SetText("") + end + else + detail.sourceFS:SetText("") + end + end + + local numReagents = API.GetNumReagents(S.selectedIndex) + local canCreate = true + for i = 1, L.MAX_REAGENTS do + if i <= numReagents then + local rName, rTex, rCount, rPC = API.GetReagentInfo(S.selectedIndex, i) + S.reagentSlots[i].icon:SetTexture(rTex) + S.reagentSlots[i].nameFS:SetText(rName or "") + + local rQuality + local rLink = API.GetReagentItemLink(S.selectedIndex, i) + if rLink then + local _, _, itemString = string.find(rLink, "item:(%d+)") + if itemString and GetItemInfo then + local _, _, scanRarity = GetItemInfo("item:" .. itemString) + rQuality = scanRarity + end + end + if rQuality and rQuality > 1 and GetItemQualityColor then + local qr, qg, qb = GetItemQualityColor(rQuality) + S.reagentSlots[i].nameFS:SetTextColor(qr, qg, qb) + else + S.reagentSlots[i].nameFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + end + + S.reagentSlots[i].countFS:SetText((rPC or 0) .. "/" .. (rCount or 0)) + if (rPC or 0) >= (rCount or 0) then + S.reagentSlots[i].countFS:SetTextColor(T.reagentOk[1], T.reagentOk[2], T.reagentOk[3]) + if rQuality and rQuality > 1 and GetItemQualityColor then + local qr, qg, qb = GetItemQualityColor(rQuality) + S.reagentSlots[i].iconFrame:SetBackdropBorderColor(qr, qg, qb, 1) + else + S.reagentSlots[i].iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + end + else + S.reagentSlots[i].countFS:SetTextColor(T.reagentLack[1], T.reagentLack[2], T.reagentLack[3]) + S.reagentSlots[i].iconFrame:SetBackdropBorderColor(T.reagentLack[1], T.reagentLack[2], T.reagentLack[3], 0.8) + canCreate = false + end + S.reagentSlots[i].reagentIndex = i; S.reagentSlots[i]:Show() + else S.reagentSlots[i]:Hide() end + end + S.MainFrame.createBtn:SetDisabled(not canCreate) + S.MainFrame.createAllBtn:SetDisabled(not canCreate or not numAvail or numAvail <= 0) +end + +function TSUI.UpdateFilters() + if not S.MainFrame then return end + S.MainFrame.filterAll:SetActive(S.currentFilter == "all") + S.MainFrame.filterAvail:SetActive(S.currentFilter == "available") + S.MainFrame.filterOptimal:SetActive(S.currentFilter == "optimal") + S.MainFrame.filterHasMat:SetActive(S.currentFilter == "hasmat") + S.MainFrame.filterUnlearned:SetActive(S.currentFilter == "unlearned") +end + +function TSUI.FullUpdate() + TSUI.UpdateFilters(); TSUI.UpdateList(); TSUI.UpdateDetail() + if TSUI.UpdateScrollbar then TSUI.UpdateScrollbar() end +end + +function TSUI.SelectRecipe(index) + S.selectedIndex = index; S.selectedUnlearned = nil + API.SelectRecipe(index) + S.craftAmount = 1 + if S.MainFrame and S.MainFrame.spinner then S.MainFrame.spinner:SetValue(1) end + TSUI.FullUpdate() +end + +function TSUI.SelectUnlearned(data) + S.selectedIndex = nil; S.selectedUnlearned = data + TSUI.UpdateDetailUnlearned(data) + if S.MainFrame then + S.MainFrame.createBtn:SetDisabled(true) + S.MainFrame.createAllBtn:SetDisabled(true) + end +end + +function TSUI.UpdateDetailUnlearned(data) + if not S.MainFrame then return end + local detail = S.MainFrame.detail + + detail.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") + detail.iconFrame:Show(); detail.qualGlow:Hide() + detail.nameFS:SetText("|cff888888" .. (data.name or "?") .. "|r") + detail.nameFS:SetTextColor(0.55, 0.55, 0.55) + detail.descFS:SetText("") + detail.cooldownFS:SetText(""); detail.toolsFS:SetText("") + detail.madeFS:SetText("|cffff5555未学习|r") + + if detail.diffDot then + if data.thresholds then + detail.diffDot:SetVertexColor(0.5, 0.5, 0.5, 1); detail.diffDot:Show() + detail.diffFS:SetText("|cffbbbbbb技能:|r " .. API.FormatThresholds(data.thresholds)) + detail.diffFS:SetTextColor(0.6, 0.6, 0.6) + else + detail.diffDot:Hide(); detail.diffFS:SetText("") + end + end + + if detail.sourceFS then + local src = data.source + if src then + local stype, sdetail = API.ParseSource(src) + local lbl = SOURCE_LABELS[stype] + if lbl then + local text = "|cffbbbbbb来源:|r " .. lbl[1] + if sdetail and sdetail ~= "" then + text = text .. " - " .. sdetail + end + detail.sourceFS:SetText(text) + else + detail.sourceFS:SetText("") + end + else + detail.sourceFS:SetText("") + end + end + + local reagents = NanamiTradeSkillReagents and data.itemID and NanamiTradeSkillReagents[data.itemID] + for i = 1, L.MAX_REAGENTS do + if reagents and i <= table.getn(reagents) then + local entry = reagents[i] + local rItemID, rCount = entry[1], entry[2] + local rName, rTex, rQuality + if GetItemInfo then + local n, _, q, _, _, _, _, _, t = GetItemInfo(rItemID) + rName = n; rTex = t; rQuality = q + end + if rName then + S.reagentSlots[i].icon:SetTexture(rTex) + S.reagentSlots[i].nameFS:SetText(rName) + if rQuality and rQuality > 1 and GetItemQualityColor then + local qr, qg, qb = GetItemQualityColor(rQuality) + S.reagentSlots[i].nameFS:SetTextColor(qr, qg, qb) + S.reagentSlots[i].iconFrame:SetBackdropBorderColor(qr, qg, qb, 1) + else + S.reagentSlots[i].nameFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + S.reagentSlots[i].iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + end + else + S.reagentSlots[i].icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") + S.reagentSlots[i].nameFS:SetText("|cff888888物品#" .. rItemID .. "|r") + S.reagentSlots[i].nameFS:SetTextColor(0.55, 0.55, 0.55) + S.reagentSlots[i].iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + end + S.reagentSlots[i].countFS:SetText("x" .. rCount) + S.reagentSlots[i].countFS:SetTextColor(0.7, 0.7, 0.7) + S.reagentSlots[i].reagentIndex = nil + S.reagentSlots[i]:Show() + else + S.reagentSlots[i]:Hide() + end + end +end + +function TSUI.ToggleCategory(catName) + if S.collapsedCats[catName] then S.collapsedCats[catName] = nil + else S.collapsedCats[catName] = true end + TSUI.FullUpdate() +end + +function TSUI.UpdateProgressBar() + local skillName, cur, mx = API.GetSkillLineName() + S.MainFrame.titleFS:SetText(skillName or "") + if mx and mx > 0 and cur then + local barWidth = S.MainFrame.progressFrame:GetWidth() - 4 + local fill = math.min(1, cur / mx) + S.MainFrame.progressBar:SetWidth(math.max(1, barWidth * fill)) + S.MainFrame.progressText:SetText(cur .. " / " .. mx) + else + S.MainFrame.progressBar:SetWidth(1) + S.MainFrame.progressText:SetText("") + end +end + +-------------------------------------------------------------------------------- +-- Profession Tabs (right-side icon strip, spellbook style) +-------------------------------------------------------------------------------- +local PROF_ALIAS = { + ["熔炼"]="采矿", ["采矿"]="熔炼", + ["Smelting"]="Mining", ["Mining"]="Smelting", +} + +function TSUI.ProfNamesMatch(a, b) + if not a or not b or a == "" or b == "" then return false end + if a == b then return true end + if string.find(a, b, 1, true) or string.find(b, a, 1, true) then return true end + local a2 = PROF_ALIAS[a] + if a2 and (a2 == b or string.find(a2, b, 1, true) or string.find(b, a2, 1, true)) then return true end + local b2 = PROF_ALIAS[b] + if b2 and (b2 == a or string.find(b2, a, 1, true) or string.find(a, b2, 1, true)) then return true end + return false +end + +function TSUI.ScanProfessions() + S.profList = {} + if not GetSpellName then return end + local seen = {} + local idx = 1 + local btype = BOOKTYPE_SPELL or "spell" + while true do + local name, rank = GetSpellName(idx, btype) + if not name then break end + if PROF_SPELLS[name] and not seen[name] then + seen[name] = true + local tex = GetSpellTexture(idx, btype) + table.insert(S.profList, { name = name, icon = tex }) + end + idx = idx + 1 + end +end + +function TSUI.CreateProfTabs(parent) + local TAB_SZ, TAB_GAP, TAB_TOP = 42, 4, 6 + for i = 1, 10 do + local tab = CreateFrame("Button", nil, parent) + tab:SetWidth(TAB_SZ); tab:SetHeight(TAB_SZ) + tab:SetPoint("TOPLEFT", parent, "TOPRIGHT", 2, + -(TAB_TOP + (i - 1) * (TAB_SZ + TAB_GAP))) + tab:SetFrameStrata("HIGH") + tab:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + tab:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + tab:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], + T.slotBorder[4]) + + local icon = tab:CreateTexture(nil, "ARTWORK") + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:SetPoint("TOPLEFT", tab, "TOPLEFT", 4, -4) + icon:SetPoint("BOTTOMRIGHT", tab, "BOTTOMRIGHT", -4, 4) + tab.icon = icon + + local glow = tab:CreateTexture(nil, "OVERLAY") + glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + glow:SetBlendMode("ADD"); glow:SetAlpha(0.7) + local gs = TAB_SZ * 1.8 + glow:SetWidth(gs); glow:SetHeight(gs) + glow:SetPoint("CENTER", tab, "CENTER", 0, 0) + glow:Hide(); tab.glow = glow + + local hl = tab:CreateTexture(nil, "HIGHLIGHT") + hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square") + hl:SetBlendMode("ADD"); hl:SetAlpha(0.3) + hl:SetPoint("TOPLEFT", icon, "TOPLEFT", 0, 0) + hl:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 0, 0) + + local checked = tab:CreateTexture(nil, "BORDER") + checked:SetTexture("Interface\\Buttons\\CheckButtonHilight") + checked:SetBlendMode("ADD"); checked:SetAlpha(0.35) + checked:SetAllPoints(tab); checked:Hide(); tab.checked = checked + + tab.profName = nil; tab.active = false; tab:Hide() + + tab:SetScript("OnEnter", function() + if this.profName then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(this.profName, 1, 0.82, 0.60) + GameTooltip:Show() + end + end) + tab:SetScript("OnLeave", function() GameTooltip:Hide() end) + tab:SetScript("OnClick", function() + if this.active or not this.profName then return end + S.switchStartTime = GetTime() + CastSpellByName(this.profName) + end) + S.profTabs[i] = tab + end +end + +function TSUI.UpdateProfTabs() + TSUI.ScanProfessions() + local currentSkillName = API.GetSkillLineName() + for i = 1, 10 do + local tab = S.profTabs[i] + if not tab then break end + local prof = S.profList[i] + if prof then + tab.profName = prof.name + tab.icon:SetTexture(prof.icon) + local isActive = TSUI.ProfNamesMatch(prof.name, currentSkillName) + tab.active = isActive + if isActive then + tab:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], + T.slotSelected[3], 1) + tab.icon:SetVertexColor(1, 1, 1) + tab.glow:SetVertexColor(T.slotSelected[1], T.slotSelected[2], + T.slotSelected[3]) + tab.glow:Show(); tab.checked:Show() + else + tab:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], + T.slotBorder[3], T.slotBorder[4]) + tab.icon:SetVertexColor(0.75, 0.75, 0.75) + tab.glow:Hide(); tab.checked:Hide() + end + tab:Show() + else + tab.profName = nil; tab.active = false; tab:Hide() + end + end +end + +function TSUI.IsTabSwitching() + return S.switchStartTime and (GetTime() - S.switchStartTime) < 1.0 +end + +-------------------------------------------------------------------------------- +-- Hide Blizzard Frames (module methods) +-------------------------------------------------------------------------------- +function TSUI.HideBlizzardTradeSkill() + if not TradeSkillFrame then return end + TradeSkillFrame:SetScript("OnHide", function() end) + if TradeSkillFrame:IsVisible() then + if HideUIPanel then pcall(HideUIPanel, TradeSkillFrame) else TradeSkillFrame:Hide() end + end + TradeSkillFrame:SetAlpha(0); TradeSkillFrame:EnableMouse(false) + TradeSkillFrame:ClearAllPoints() + TradeSkillFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) +end + +function TSUI.HideBlizzardCraft() + if not CraftFrame then return end + CraftFrame:SetScript("OnHide", function() end) + if CraftFrame:IsVisible() then + if HideUIPanel then pcall(HideUIPanel, CraftFrame) else CraftFrame:Hide() end + end + CraftFrame:SetAlpha(0); CraftFrame:EnableMouse(false) + CraftFrame:ClearAllPoints() + CraftFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) +end + +function TSUI.CleanupBlizzardTradeSkill() + if not TradeSkillFrame then return end + TradeSkillFrame:SetScript("OnHide", function() end) + if HideUIPanel then pcall(HideUIPanel, TradeSkillFrame) end + if TradeSkillFrame:IsVisible() then TradeSkillFrame:Hide() end + TradeSkillFrame:SetAlpha(0); TradeSkillFrame:EnableMouse(false) +end + +function TSUI.CleanupBlizzardCraft() + if not CraftFrame then return end + CraftFrame:SetScript("OnHide", function() end) + if HideUIPanel then pcall(HideUIPanel, CraftFrame) end + if CraftFrame:IsVisible() then CraftFrame:Hide() end + CraftFrame:SetAlpha(0); CraftFrame:EnableMouse(false) +end + +-------------------------------------------------------------------------------- +-- Initialize (left-right layout: list on left, detail on right) +-------------------------------------------------------------------------------- +function TSUI:Initialize() + if S.MainFrame then return end + local MF = CreateFrame("Frame", "SFramesTradeSkillFrame", UIParent) + S.MainFrame = MF + MF:SetWidth(L.FRAME_W); MF:SetHeight(L.FRAME_H) + MF:SetPoint("LEFT", UIParent, "LEFT", 36, 0) + MF:SetFrameStrata("HIGH"); MF:SetToplevel(true); MF:EnableMouse(true) + MF:SetMovable(true); MF:RegisterForDrag("LeftButton") + MF:SetScript("OnDragStart", function() this:StartMoving() end) + MF:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + TSUI.SetRoundBackdrop(MF); TSUI.CreateShadow(MF) + + local font = TSUI.GetFont() + + -- ═══ Header (full width) ═════════════════════════════════════════ + local header = CreateFrame("Frame", nil, MF) + header:SetPoint("TOPLEFT", 0, 0); header:SetPoint("TOPRIGHT", 0, 0) + header:SetHeight(L.HEADER_H) + header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) + header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) + + local titleFS = header:CreateFontString(nil, "OVERLAY") + local titleIco = SFrames:CreateIcon(header, "profession", 16) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("TOPLEFT", header, "TOPLEFT", L.SIDE_PAD, -8) + titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) + + titleFS:SetFont(font, 14, "OUTLINE") + titleFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) + titleFS:SetPoint("RIGHT", header, "RIGHT", -30, 0) + titleFS:SetJustifyH("LEFT"); titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + MF.titleFS = titleFS + + local pf = CreateFrame("Frame", nil, header) + pf:SetPoint("BOTTOMLEFT", header, "BOTTOMLEFT", L.SIDE_PAD, 5) + pf:SetPoint("BOTTOMRIGHT", header, "BOTTOMRIGHT", -L.SIDE_PAD - 24, 5) + pf:SetHeight(10) + pf:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) + pf:SetBackdropColor(T.progressBg[1], T.progressBg[2], T.progressBg[3], T.progressBg[4]) + pf:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.5) + + local pb = pf:CreateTexture(nil, "ARTWORK") + pb:SetTexture("Interface\\Buttons\\WHITE8X8") + pb:SetPoint("TOPLEFT", pf, "TOPLEFT", 2, -2); pb:SetPoint("BOTTOMLEFT", pf, "BOTTOMLEFT", 2, 2) + pb:SetWidth(1); pb:SetVertexColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], T.progressFill[4]) + MF.progressBar = pb; MF.progressFrame = pf + + local pt = pf:CreateFontString(nil, "OVERLAY") + pt:SetFont(font, 8, "OUTLINE"); pt:SetPoint("CENTER", pf, "CENTER", 0, 0) + pt:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]); MF.progressText = pt + + local closeBtn = CreateFrame("Button", nil, header) + closeBtn:SetWidth(20); closeBtn:SetHeight(20); closeBtn:SetPoint("TOPRIGHT", header, "TOPRIGHT", -8, -6) + local closeTex = closeBtn:CreateTexture(nil, "ARTWORK") + closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") + closeTex:SetTexCoord(0.25, 0.375, 0, 0.125); closeTex:SetAllPoints() + closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) + closeBtn:SetScript("OnClick", function() S.MainFrame:Hide() end) + closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end) + closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) + + -- Separators + local hsep = MF:CreateTexture(nil, "ARTWORK") + hsep:SetTexture("Interface\\Buttons\\WHITE8X8"); hsep:SetHeight(1) + hsep:SetPoint("TOPLEFT", MF, "TOPLEFT", 4, -L.HEADER_H) + hsep:SetPoint("TOPRIGHT", MF, "TOPRIGHT", -4, -L.HEADER_H) + hsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + local vdiv = MF:CreateTexture(nil, "ARTWORK") + vdiv:SetTexture("Interface\\Buttons\\WHITE8X8"); vdiv:SetWidth(1) + vdiv:SetPoint("TOP", MF, "TOPLEFT", L.LEFT_W, -(L.HEADER_H + 2)) + vdiv:SetPoint("BOTTOM", MF, "BOTTOMLEFT", L.LEFT_W, 4) + vdiv:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + -- ═══ LEFT PANEL: Filters + Recipe List ═══════════════════════════ + + local fb = CreateFrame("Frame", nil, MF) + fb:SetPoint("TOPLEFT", MF, "TOPLEFT", L.SIDE_PAD, -(L.HEADER_H + 4)) + fb:SetWidth(L.LEFT_W - L.SIDE_PAD * 2); fb:SetHeight(22) + + local fAll = TSUI.CreateFilterBtn(fb, "全部", 38) + fAll:SetPoint("LEFT", fb, "LEFT", 0, 0) + fAll:SetScript("OnClick", function() S.currentFilter = "all"; TSUI.FullUpdate() end) + MF.filterAll = fAll + + local fAvail = TSUI.CreateFilterBtn(fb, "可做", 38) + fAvail:SetPoint("LEFT", fAll, "RIGHT", 3, 0) + fAvail:SetScript("OnClick", function() S.currentFilter = "available"; TSUI.FullUpdate() end) + MF.filterAvail = fAvail + + local fOpt = TSUI.CreateFilterBtn(fb, "橙/黄", 42) + fOpt:SetPoint("LEFT", fAvail, "RIGHT", 3, 0) + fOpt:SetScript("OnClick", function() S.currentFilter = "optimal"; TSUI.FullUpdate() end) + MF.filterOptimal = fOpt + + local fMat = TSUI.CreateFilterBtn(fb, "有材料", 46) + fMat:SetPoint("LEFT", fOpt, "RIGHT", 3, 0) + fMat:SetScript("OnClick", function() S.currentFilter = "hasmat"; TSUI.FullUpdate() end) + MF.filterHasMat = fMat + + local fUnlearned = TSUI.CreateFilterBtn(fb, "未学", 38) + fUnlearned:SetPoint("LEFT", fMat, "RIGHT", 3, 0) + fUnlearned:SetScript("OnClick", function() S.currentFilter = "unlearned"; TSUI.FullUpdate() end) + MF.filterUnlearned = fUnlearned + + local sb = CreateFrame("EditBox", "SFramesTSSearchBox", MF) + sb:SetPoint("TOPLEFT", MF, "TOPLEFT", L.SIDE_PAD, -(L.HEADER_H + 28)) + sb:SetWidth(L.LEFT_W - L.SIDE_PAD * 2); sb:SetHeight(20) + sb:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) + sb:SetBackdropColor(T.searchBg[1], T.searchBg[2], T.searchBg[3], T.searchBg[4]) + sb:SetBackdropBorderColor(T.searchBorder[1], T.searchBorder[2], T.searchBorder[3], T.searchBorder[4]) + sb:SetFont(font, 11); sb:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + sb:SetTextInsets(6, 6, 2, 2); sb:SetAutoFocus(false); sb:SetMaxLetters(30) + sb:SetScript("OnEscapePressed", function() this:ClearFocus() end) + sb:SetScript("OnEnterPressed", function() this:ClearFocus() end) + sb:SetScript("OnTextChanged", function() S.searchText = this:GetText() or ""; TSUI.FullUpdate() end) + MF.searchBox = sb + + local sLabel = sb:CreateFontString(nil, "OVERLAY") + sLabel:SetFont(font, 11, "OUTLINE"); sLabel:SetPoint("LEFT", sb, "LEFT", 6, 0) + sLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3], 0.6); sLabel:SetText("搜索...") + MF.searchLabel = sLabel + sb:SetScript("OnEditFocusGained", function() sLabel:Hide() end) + sb:SetScript("OnEditFocusLost", function() if (this:GetText() or "") == "" then sLabel:Show() end end) + + -- List scroll area (fills left panel below filters) + local listTop = L.HEADER_H + L.FILTER_H + 4 + local ls = CreateFrame("ScrollFrame", "SFramesTSListScroll", MF) + ls:SetPoint("TOPLEFT", MF, "TOPLEFT", L.SIDE_PAD, -listTop) + ls:SetPoint("BOTTOMRIGHT", MF, "BOTTOMLEFT", L.SIDE_PAD + L.LIST_ROW_W, 6) + + local lc = CreateFrame("Frame", "SFramesTSListContent", ls) + lc:SetWidth(L.LIST_ROW_W); lc:SetHeight(1) + ls:SetScrollChild(lc) + ls:EnableMouseWheel(true) + + -- Scrollbar track + local sbTrack = CreateFrame("Frame", nil, MF) + sbTrack:SetWidth(L.SCROLLBAR_W) + sbTrack:SetPoint("TOPLEFT", ls, "TOPRIGHT", 2, 0) + sbTrack:SetPoint("BOTTOMLEFT", ls, "BOTTOMRIGHT", 2, 0) + sbTrack:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, + edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) + sbTrack:SetBackdropColor(T.progressBg[1], T.progressBg[2], T.progressBg[3], 0.6) + sbTrack:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.3) + + -- Scrollbar thumb + local sbThumb = CreateFrame("Button", nil, sbTrack) + sbThumb:SetWidth(L.SCROLLBAR_W - 2); sbThumb:SetHeight(30) + sbThumb:SetPoint("TOP", sbTrack, "TOP", 0, -1) + sbThumb:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, + edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) + sbThumb:SetBackdropColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 0.7) + sbThumb:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.5) + sbThumb:EnableMouse(true); sbThumb:SetMovable(true) + sbThumb:RegisterForDrag("LeftButton") + sbThumb._dragging = false + + sbThumb:SetScript("OnEnter", function() + this:SetBackdropColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 1) + end) + sbThumb:SetScript("OnLeave", function() + this:SetBackdropColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 0.7) + end) + sbThumb:SetScript("OnDragStart", function() + this._dragging = true + this._startY = select(2, GetCursorPosition()) / (this:GetEffectiveScale()) + this._startScroll = ls:GetVerticalScroll() + end) + sbThumb:SetScript("OnDragStop", function() this._dragging = false end) + sbThumb:SetScript("OnUpdate", function() + if not this._dragging then return end + local cursorY = select(2, GetCursorPosition()) / (this:GetEffectiveScale()) + local delta = this._startY - cursorY + local trackH = sbTrack:GetHeight() - this:GetHeight() + if trackH <= 0 then return end + local scrollMax = TSUI.GetScrollMax() + local newScroll = this._startScroll + (delta / trackH) * scrollMax + newScroll = math.max(0, math.min(scrollMax, newScroll)) + ls:SetVerticalScroll(newScroll) + TSUI.UpdateScrollbar() + end) + + sbTrack:EnableMouse(true) + sbTrack:SetScript("OnMouseDown", function() + local trackTop = sbTrack:GetTop() + local cursorY = select(2, GetCursorPosition()) / (sbTrack:GetEffectiveScale()) + local clickRatio = (trackTop - cursorY) / sbTrack:GetHeight() + clickRatio = math.max(0, math.min(1, clickRatio)) + ls:SetVerticalScroll(clickRatio * TSUI.GetScrollMax()) + TSUI.UpdateScrollbar() + end) + + MF.sbTrack = sbTrack; MF.sbThumb = sbThumb + + function TSUI.GetScrollMax() + local contentH = ls.content and ls.content:GetHeight() or 0 + local viewH = ls:GetHeight() or 0 + return math.max(0, contentH - viewH) + end + + function TSUI.UpdateScrollbar() + if not MF.sbThumb or not MF.sbTrack then return end + local scrollMax = TSUI.GetScrollMax() + if scrollMax <= 0 then MF.sbThumb:Hide(); return end + MF.sbThumb:Show() + local trackH = MF.sbTrack:GetHeight() + local curScroll = ls:GetVerticalScroll() + local ratio = curScroll / scrollMax + ratio = math.max(0, math.min(1, ratio)) + local thumbH = math.max(20, trackH * (trackH / (trackH + scrollMax))) + MF.sbThumb:SetHeight(thumbH) + local maxOffset = trackH - thumbH - 2 + MF.sbThumb:ClearAllPoints() + MF.sbThumb:SetPoint("TOP", MF.sbTrack, "TOP", 0, -(1 + ratio * maxOffset)) + end + + ls:SetScript("OnMouseWheel", function() + local cur = this:GetVerticalScroll(); local mx = TSUI.GetScrollMax() + if arg1 > 0 then this:SetVerticalScroll(math.max(0, cur - L.SCROLL_STEP)) + else this:SetVerticalScroll(math.min(mx, cur + L.SCROLL_STEP)) end + TSUI.UpdateScrollbar() + end) + ls:SetScript("OnScrollRangeChanged", function() TSUI.UpdateScrollbar() end) + ls.content = lc; MF.listScroll = ls + + for i = 1, L.MAX_ROWS do + local row = TSUI.CreateListRow(lc, i) + row:SetWidth(L.LIST_ROW_W) + row:EnableMouseWheel(true) + row:SetScript("OnMouseWheel", function() + local sf = S.MainFrame.listScroll + local cur = sf:GetVerticalScroll() + local mx = TSUI.GetScrollMax() + if arg1 > 0 then sf:SetVerticalScroll(math.max(0, cur - L.SCROLL_STEP)) + else sf:SetVerticalScroll(math.min(mx, cur + L.SCROLL_STEP)) end + TSUI.UpdateScrollbar() + end) + row:SetScript("OnClick", function() + if IsShiftKeyDown() and this.recipeIndex and not this.isHeader then + TSUI.LinkRecipeToChat(this.recipeIndex) + elseif IsShiftKeyDown() and this.unlearnedData then + TSUI.LinkUnlearnedToChat(this.unlearnedData) + elseif this.isHeader and this.catName then + TSUI.ToggleCategory(this.catName) + elseif this.recipeIndex then + TSUI.SelectRecipe(this.recipeIndex) + elseif this.unlearnedData then + TSUI.SelectUnlearned(this.unlearnedData) + end + end) + S.rowButtons[i] = row + end + + -- ═══ RIGHT PANEL: Recipe Detail ══════════════════════════════════ + + local rightX = L.LEFT_W + L.SIDE_PAD + local rightW = L.CONTENT_W + + local det = CreateFrame("Frame", nil, MF) + det:SetPoint("TOPLEFT", MF, "TOPLEFT", rightX, -(L.HEADER_H + 6)) + det:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -L.SIDE_PAD, L.BOTTOM_H + 2) + MF.detail = det + + -- Recipe icon + local dIF = CreateFrame("Frame", nil, det) + dIF:SetWidth(40); dIF:SetHeight(40); dIF:SetPoint("TOPLEFT", det, "TOPLEFT", 0, 0) + dIF:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 2, right = 2, top = 2, bottom = 2 } }) + dIF:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + dIF:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + dIF:Hide(); det.iconFrame = dIF + + local dIcon = dIF:CreateTexture(nil, "ARTWORK") + dIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + dIcon:SetPoint("TOPLEFT", dIF, "TOPLEFT", 3, -3) + dIcon:SetPoint("BOTTOMRIGHT", dIF, "BOTTOMRIGHT", -3, 3) + det.icon = dIcon + + local dGlow = dIF:CreateTexture(nil, "OVERLAY") + dGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + dGlow:SetBlendMode("ADD"); dGlow:SetAlpha(0.8) + dGlow:SetWidth(80); dGlow:SetHeight(80) + dGlow:SetPoint("CENTER", dIF, "CENTER", 0, 0) + dGlow:Hide(); det.qualGlow = dGlow + + dIF:EnableMouse(true) + dIF:SetScript("OnEnter", function() + if S.selectedIndex then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + local ok + if S.currentMode == "craft" then + local link = GetCraftItemLink and GetCraftItemLink(S.selectedIndex) + if link then + ok = pcall(GameTooltip.SetCraftItem, GameTooltip, S.selectedIndex) + else + ok = pcall(GameTooltip.SetCraftSpell, GameTooltip, S.selectedIndex) + end + else ok = pcall(GameTooltip.SetTradeSkillItem, GameTooltip, S.selectedIndex) end + if ok then GameTooltip:Show() else GameTooltip:Hide() end + end + end) + dIF:SetScript("OnLeave", function() GameTooltip:Hide() end) + + local dName = det:CreateFontString(nil, "OVERLAY") + dName:SetFont(font, 13, "OUTLINE") + dName:SetPoint("TOPLEFT", dIF, "TOPRIGHT", 8, -2) + dName:SetPoint("RIGHT", det, "RIGHT", -4, 0) + dName:SetJustifyH("LEFT"); dName:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + det.nameFS = dName + + local dMade = det:CreateFontString(nil, "OVERLAY") + dMade:SetFont(font, 10, "OUTLINE") + dMade:SetPoint("TOPLEFT", dName, "BOTTOMLEFT", 0, -2) + dMade:SetJustifyH("LEFT"); det.madeFS = dMade + + local dCD = det:CreateFontString(nil, "OVERLAY") + dCD:SetFont(font, 10, "OUTLINE") + dCD:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -48) + dCD:SetPoint("RIGHT", det, "RIGHT", -4, 0) + dCD:SetJustifyH("LEFT"); det.cooldownFS = dCD + + local dTools = det:CreateFontString(nil, "OVERLAY") + dTools:SetFont(font, 10, "OUTLINE") + dTools:SetPoint("TOPLEFT", dCD, "BOTTOMLEFT", 0, -2) + dTools:SetPoint("RIGHT", det, "RIGHT", -4, 0) + dTools:SetJustifyH("LEFT"); det.toolsFS = dTools + + local dDesc = det:CreateFontString(nil, "OVERLAY") + dDesc:SetFont(font, 10) + dDesc:SetPoint("TOPLEFT", dTools, "BOTTOMLEFT", 0, -2) + dDesc:SetPoint("RIGHT", det, "RIGHT", -4, 0) + dDesc:SetJustifyH("LEFT") + dDesc:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]); det.descFS = dDesc + + -- ── Current Difficulty ────────────────────────────────────────── + local diffSep1 = det:CreateTexture(nil, "ARTWORK") + diffSep1:SetTexture("Interface\\Buttons\\WHITE8X8"); diffSep1:SetHeight(1) + diffSep1:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -86) + diffSep1:SetPoint("RIGHT", det, "RIGHT", 0, 0) + diffSep1:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) + + local diffDot = det:CreateTexture(nil, "ARTWORK") + diffDot:SetTexture("Interface\\Buttons\\WHITE8X8") + diffDot:SetWidth(10); diffDot:SetHeight(10) + diffDot:SetPoint("TOPLEFT", det, "TOPLEFT", 2, -94) + diffDot:Hide(); det.diffDot = diffDot + + local diffFS = det:CreateFontString(nil, "OVERLAY") + diffFS:SetFont(font, 11, "OUTLINE") + diffFS:SetPoint("LEFT", diffDot, "RIGHT", 6, 0) + diffFS:SetJustifyH("LEFT"); det.diffFS = diffFS + + local sourceFS = det:CreateFontString(nil, "OVERLAY") + sourceFS:SetFont(font, 10, "OUTLINE") + sourceFS:SetPoint("TOPLEFT", det, "TOPLEFT", 2, -108) + sourceFS:SetJustifyH("LEFT"); det.sourceFS = sourceFS + + -- ── Reagent Section ───────────────────────────────────────────── + local reagSep = det:CreateTexture(nil, "ARTWORK") + reagSep:SetTexture("Interface\\Buttons\\WHITE8X8"); reagSep:SetHeight(1) + reagSep:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -126) + reagSep:SetPoint("RIGHT", det, "RIGHT", 0, 0) + reagSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) + + local rLabel = det:CreateFontString(nil, "OVERLAY") + rLabel:SetFont(font, 10, "OUTLINE") + rLabel:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -132) + rLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) + rLabel:SetText("所需材料:") + + local rStartY = -150 + for i = 1, L.MAX_REAGENTS do + local slot = TSUI.CreateReagentSlot(det, i) + local col = math.mod(i - 1, 2) + local ri = math.floor((i - 1) / 2) + slot:SetPoint("TOPLEFT", det, "TOPLEFT", col * (rightW / 2 + 2), rStartY - ri * 34) + S.reagentSlots[i] = slot + end + + -- ── Bottom Bar (right panel bottom) ───────────────────────────── + local bsep = MF:CreateTexture(nil, "ARTWORK") + bsep:SetTexture("Interface\\Buttons\\WHITE8X8"); bsep:SetHeight(1) + bsep:SetPoint("BOTTOMLEFT", MF, "BOTTOMLEFT", L.LEFT_W + 4, L.BOTTOM_H) + bsep:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -4, L.BOTTOM_H) + bsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + MF.spinner = TSUI.CreateSpinner(MF, rightX, 8) + + local cBtn = TSUI.CreateActionBtn(MF, "制作", 70) + cBtn:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -(L.SIDE_PAD + 76), 8) + cBtn:SetScript("OnClick", function() + if this.disabled then return end + if S.selectedIndex then + local amt = S.MainFrame.spinner:GetValue() + if S.currentMode == "craft" then + for ci = 1, amt do API.DoRecipe(S.selectedIndex, 1) end + else API.DoRecipe(S.selectedIndex, amt) end + end + end) + MF.createBtn = cBtn + + local caBtn = TSUI.CreateActionBtn(MF, "全部制作", 70) + caBtn:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -L.SIDE_PAD, 8) + caBtn:SetScript("OnClick", function() + if this.disabled then return end + if S.selectedIndex then + local _, _, na = API.GetRecipeInfo(S.selectedIndex) + if na and na > 0 then + if S.currentMode == "craft" then + for ci = 1, na do API.DoRecipe(S.selectedIndex, 1) end + else API.DoRecipe(S.selectedIndex, na) end + end + end + end) + MF.createAllBtn = caBtn + + -- ═══ Events ══════════════════════════════════════════════════════ + MF:SetScript("OnHide", function() + S.switchStartTime = nil + API.CloseRecipe() + if S.currentMode == "tradeskill" then TSUI.CleanupBlizzardTradeSkill() + else TSUI.CleanupBlizzardCraft() end + end) + + MF:RegisterEvent("TRADE_SKILL_SHOW") + MF:RegisterEvent("TRADE_SKILL_UPDATE") + MF:RegisterEvent("TRADE_SKILL_CLOSE") + MF:RegisterEvent("CRAFT_SHOW") + MF:RegisterEvent("CRAFT_UPDATE") + MF:RegisterEvent("CRAFT_CLOSE") + MF:SetScript("OnEvent", function() + if event == "TRADE_SKILL_SHOW" then + S.switchStartTime = nil + S.currentMode = "tradeskill" + if TradeSkillFrame then + TradeSkillFrame:SetScript("OnHide", function() end) + TradeSkillFrame:SetAlpha(0); TradeSkillFrame:EnableMouse(false) + end + TSUI.ResetAndShow() + S.MainFrame._hideBlizzTimer = 0 + S.MainFrame:SetScript("OnUpdate", function() + if not this._hideBlizzTimer then return end + this._hideBlizzTimer = this._hideBlizzTimer + arg1 + if this._hideBlizzTimer > 0.05 then + this._hideBlizzTimer = nil; this:SetScript("OnUpdate", nil) + TSUI.CleanupBlizzardTradeSkill() + end + end) + elseif event == "TRADE_SKILL_UPDATE" then + if S.MainFrame:IsVisible() and S.currentMode == "tradeskill" then + TSUI.UpdateProgressBar(); TSUI.FullUpdate() + end + elseif event == "TRADE_SKILL_CLOSE" then + TSUI.CleanupBlizzardTradeSkill() + if TSUI.IsTabSwitching() then + -- switching: keep panel open + else + S.MainFrame._hideBlizzTimer = nil + S.MainFrame:SetScript("OnUpdate", nil) + S.MainFrame:Hide() + end + elseif event == "CRAFT_SHOW" then + S.switchStartTime = nil + S.currentMode = "craft" + if CraftFrame then + CraftFrame:SetScript("OnHide", function() end) + CraftFrame:SetAlpha(0); CraftFrame:EnableMouse(false) + end + TSUI.ResetAndShow() + S.MainFrame._hideBlizzTimer = 0 + S.MainFrame:SetScript("OnUpdate", function() + if not this._hideBlizzTimer then return end + this._hideBlizzTimer = this._hideBlizzTimer + arg1 + if this._hideBlizzTimer > 0.05 then + this._hideBlizzTimer = nil; this:SetScript("OnUpdate", nil) + TSUI.CleanupBlizzardCraft() + end + end) + elseif event == "CRAFT_UPDATE" then + if S.MainFrame:IsVisible() and S.currentMode == "craft" then + TSUI.UpdateProgressBar(); TSUI.FullUpdate() + end + elseif event == "CRAFT_CLOSE" then + TSUI.CleanupBlizzardCraft() + if TSUI.IsTabSwitching() then + -- switching: keep panel open + else + S.MainFrame._hideBlizzTimer = nil + S.MainFrame:SetScript("OnUpdate", nil) + S.MainFrame:Hide() + end + end + end) + TSUI.CreateProfTabs(MF) + + MF:Hide() + tinsert(UISpecialFrames, "SFramesTradeSkillFrame") +end + +function TSUI.ResetAndShow() + if NanamiTradeSkillDB_Init then NanamiTradeSkillDB_Init() end + S.selectedIndex = nil; S.selectedUnlearned = nil; S.currentFilter = "all"; S.searchText = "" + S.collapsedCats = {}; S.craftAmount = 1 + if S.MainFrame.searchBox then S.MainFrame.searchBox:SetText("") end + if S.MainFrame.spinner then S.MainFrame.spinner:SetValue(1) end + if S.MainFrame.listScroll then S.MainFrame.listScroll:SetVerticalScroll(0) end + TSUI.UpdateProgressBar(); S.MainFrame:Show(); TSUI.FullUpdate() + TSUI.UpdateScrollbar() + TSUI.UpdateProfTabs() + for _, entry in ipairs(S.displayList) do + if entry.type == "recipe" then + TSUI.SelectRecipe(entry.data.index); break + end + end +end + +-------------------------------------------------------------------------------- +-- Bootstrap +-------------------------------------------------------------------------------- +local bootstrap = CreateFrame("Frame") +bootstrap:RegisterEvent("PLAYER_LOGIN") +bootstrap:RegisterEvent("ADDON_LOADED") +bootstrap:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + if SFramesDB.enableTradeSkill == nil then SFramesDB.enableTradeSkill = true end + if SFramesDB.enableTradeSkill ~= false then TSUI:Initialize() end + elseif event == "ADDON_LOADED" then + if arg1 == "Blizzard_TradeSkillUI" then + if S.MainFrame and TradeSkillFrame then TSUI.HideBlizzardTradeSkill() end + elseif arg1 == "Blizzard_CraftUI" then + if S.MainFrame and CraftFrame then TSUI.HideBlizzardCraft() end + end + end +end) diff --git a/TrainerUI.lua b/TrainerUI.lua new file mode 100644 index 0000000..53109cb --- /dev/null +++ b/TrainerUI.lua @@ -0,0 +1,1234 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Trainer UI (TrainerUI.lua) +-- Replaces ClassTrainerFrame with Nanami-UI styled interface +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.TrainerUI = {} +local TUI = SFrames.TrainerUI +SFramesDB = SFramesDB or {} + +-------------------------------------------------------------------------------- +-- Theme (Pink Cat-Paw) +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + moneyGold = { 1, 0.84, 0.0 }, + moneySilver = { 0.78, 0.78, 0.78 }, + moneyCopper = { 0.71, 0.43, 0.18 }, + available = { 0.25, 1.0, 0.25 }, + unavailable = { 0.80, 0.20, 0.20 }, + used = { 0.50, 0.50, 0.50 }, +}) + +local QUALITY_COLORS = { + [0] = { 0.62, 0.62, 0.62 }, [1] = { 1, 1, 1 }, + [2] = { 0.12, 1, 0 }, [3] = { 0.0, 0.44, 0.87 }, + [4] = { 0.64, 0.21, 0.93 }, [5] = { 1, 0.5, 0 }, +} + +local function ColorToQuality(r, g, b) + if not r then return nil end + if r > 0.9 and g > 0.35 and g < 0.65 and b < 0.15 then return 5 end + if r > 0.5 and r < 0.8 and g < 0.35 and b > 0.8 then return 4 end + if r < 0.15 and g > 0.3 and g < 0.6 and b > 0.7 then return 3 end + if r < 0.25 and g > 0.85 and b < 0.15 then return 2 end + if r > 0.5 and r < 0.75 and g > 0.5 and g < 0.75 and b > 0.5 and b < 0.75 then return 0 end + return 1 +end + +-------------------------------------------------------------------------------- +-- Layout +-------------------------------------------------------------------------------- +local FRAME_W = 380 +local FRAME_H = 540 +local HEADER_H = 34 +local SIDE_PAD = 14 +local CONTENT_W = FRAME_W - SIDE_PAD * 2 +local FILTER_H = 28 +local LIST_ROW_H = 32 +local CAT_ROW_H = 20 +local DETAIL_H = 160 +local BOTTOM_H = 52 +local SCROLL_STEP = 40 +local MAX_ROWS = 60 + +-------------------------------------------------------------------------------- +-- State +-------------------------------------------------------------------------------- +local MainFrame = nil +local selectedIndex = nil +local currentFilter = "all" +local displayList = {} +local rowButtons = {} +local collapsedCats = {} +local function HideBlizzardTrainer() + if not ClassTrainerFrame then return end + ClassTrainerFrame:SetScript("OnHide", function() end) + if ClassTrainerFrame:IsVisible() then + if HideUIPanel then + pcall(HideUIPanel, ClassTrainerFrame) + else + ClassTrainerFrame:Hide() + end + end + ClassTrainerFrame:SetAlpha(0) + ClassTrainerFrame:EnableMouse(false) + ClassTrainerFrame:ClearAllPoints() + ClassTrainerFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) +end + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) +end + +local function CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.45) + s:SetBackdropBorderColor(0, 0, 0, 0.6) + s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) + return s +end + +local function FormatMoneyText(copper) + if not copper or copper <= 0 then return "" end + local g = math.floor(copper / 10000) + local s = math.floor(math.mod(copper, 10000) / 100) + local c = math.mod(copper, 100) + local parts = {} + if g > 0 then table.insert(parts, "|cffffd700" .. g .. "g|r") end + if s > 0 then table.insert(parts, "|cffc7c7cf" .. s .. "s|r") end + if c > 0 then table.insert(parts, "|cffeda55f" .. c .. "c|r") end + if table.getn(parts) == 0 then return "|cffc7c7cf0c|r" end + return table.concat(parts, " ") +end + +local function SetMoneyFrame(moneyFrame, value) + if not moneyFrame then return end + value = tonumber(value) or 0 + if value < 0 then value = 0 end + if SmallMoneyFrame_SetAmount then + pcall(SmallMoneyFrame_SetAmount, moneyFrame, value) + elseif MoneyFrame_Update and moneyFrame.GetName then + local name = moneyFrame:GetName() + if name and name ~= "" then + pcall(MoneyFrame_Update, name, value) + end + end +end + +local function IsServiceHeader(index) + local name, _, category = GetTrainerServiceInfo(index) + if not name then return false end + local hasIcon = GetTrainerServiceIcon and GetTrainerServiceIcon(index) + if not hasIcon or hasIcon == "" then return true end + local ok, cost = pcall(GetTrainerServiceCost, index) + if not ok then return true end + if category ~= "available" and category ~= "unavailable" and category ~= "used" then + return true + end + return false +end + +local scanTip = nil + +local function GetServiceTooltipInfo(index) + if not scanTip then + scanTip = CreateFrame("GameTooltip", "SFramesTrainerScanTip", nil, "GameTooltipTemplate") + end + scanTip:SetOwner(WorldFrame, "ANCHOR_NONE") + scanTip:ClearLines() + local ok = pcall(scanTip.SetTrainerService, scanTip, index) + if not ok then return "", "" end + + local infoLines = {} + local descLines = {} + local numLines = scanTip:NumLines() + local foundDesc = false + + for i = 2, numLines do + local leftFS = _G["SFramesTrainerScanTipTextLeft" .. i] + local rightFS = _G["SFramesTrainerScanTipTextRight" .. i] + local leftText = leftFS and leftFS:GetText() or "" + local rightText = rightFS and rightFS:GetText() or "" + + if leftText == "" and rightText == "" then + if not foundDesc and table.getn(infoLines) > 0 then + foundDesc = true + end + else + local line + if rightText ~= "" and leftText ~= "" then + line = leftText .. " " .. rightText + elseif leftText ~= "" then + line = leftText + else + line = rightText + end + + local isYellow = leftFS and leftFS.GetTextColor and true + local r, g, b + if leftFS and leftFS.GetTextColor then + r, g, b = leftFS:GetTextColor() + end + local isWhiteOrYellow = r and (r > 0.9 and g > 0.75) + + if not foundDesc and rightText ~= "" then + table.insert(infoLines, line) + elseif not foundDesc and not isWhiteOrYellow and string.len(leftText) < 30 then + table.insert(infoLines, line) + else + foundDesc = true + table.insert(descLines, line) + end + end + end + scanTip:Hide() + return table.concat(infoLines, "\n"), table.concat(descLines, "\n") +end + +local function GetServiceQuality(index) + if not scanTip then + scanTip = CreateFrame("GameTooltip", "SFramesTrainerScanTip", nil, "GameTooltipTemplate") + end + scanTip:SetOwner(WorldFrame, "ANCHOR_NONE") + scanTip:ClearLines() + local ok = pcall(scanTip.SetTrainerService, scanTip, index) + if not ok then scanTip:Hide() return nil end + local firstLine = _G["SFramesTrainerScanTipTextLeft1"] + if not firstLine or not firstLine.GetTextColor then + scanTip:Hide() + return nil + end + local r, g, b = firstLine:GetTextColor() + scanTip:Hide() + return ColorToQuality(r, g, b) +end + +-------------------------------------------------------------------------------- +-- Filter Button Factory +-------------------------------------------------------------------------------- +local function CreateFilterBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 60) + btn:SetHeight(20) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.5) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 11, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text) + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + btn.label = fs + + btn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + end) + btn:SetScript("OnLeave", function() + if this.active then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1) + else + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.5) + end + end) + + function btn:SetActive(flag) + self.active = flag + if flag then + self:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + self:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1) + self.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + else + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.5) + self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end + + return btn +end + +-------------------------------------------------------------------------------- +-- Action Button Factory +-------------------------------------------------------------------------------- +local function CreateActionBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 100) + btn:SetHeight(28) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 12, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text) + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + btn.label = fs + + btn.disabled = false + function btn:SetDisabled(flag) + self.disabled = flag + if flag then + self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) + else + self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + end + end + + btn:SetScript("OnEnter", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end + end) + btn:SetScript("OnLeave", function() + if not this.disabled then + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end) + + return btn +end + +-------------------------------------------------------------------------------- +-- List Row Factory (reusable for both headers and services) +-------------------------------------------------------------------------------- +local function CreateListRow(parent, idx) + local row = CreateFrame("Button", nil, parent) + row:SetWidth(CONTENT_W) + row:SetHeight(LIST_ROW_H) + + local iconFrame = CreateFrame("Frame", nil, row) + iconFrame:SetWidth(LIST_ROW_H - 4) + iconFrame:SetHeight(LIST_ROW_H - 4) + iconFrame:SetPoint("LEFT", row, "LEFT", 0, 0) + iconFrame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + row.iconFrame = iconFrame + + local icon = iconFrame:CreateTexture(nil, "ARTWORK") + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 3, -3) + icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -3, 3) + row.icon = icon + + local qualGlow = iconFrame:CreateTexture(nil, "OVERLAY") + qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + qualGlow:SetBlendMode("ADD") + qualGlow:SetAlpha(0.7) + qualGlow:SetWidth((LIST_ROW_H - 4) * 1.8) + qualGlow:SetHeight((LIST_ROW_H - 4) * 1.8) + qualGlow:SetPoint("CENTER", iconFrame, "CENTER", 0, 0) + qualGlow:Hide() + row.qualGlow = qualGlow + + local nameFS = row:CreateFontString(nil, "OVERLAY") + nameFS:SetFont(GetFont(), 12, "OUTLINE") + nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 6, 2) + nameFS:SetPoint("RIGHT", row, "RIGHT", -90, 0) + nameFS:SetJustifyH("LEFT") + nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + row.nameFS = nameFS + + local subFS = row:CreateFontString(nil, "OVERLAY") + subFS:SetFont(GetFont(), 10, "OUTLINE") + subFS:SetPoint("TOPLEFT", nameFS, "BOTTOMLEFT", 0, -1) + subFS:SetJustifyH("LEFT") + subFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + row.subFS = subFS + + local costFS = row:CreateFontString(nil, "OVERLAY") + costFS:SetFont(GetFont(), 11, "OUTLINE") + costFS:SetPoint("RIGHT", row, "RIGHT", -4, 0) + costFS:SetJustifyH("RIGHT") + costFS:Hide() + row.costFS = costFS + + local costMoney = CreateFrame("Frame", "SFTRow" .. idx .. "Money", row, "SmallMoneyFrameTemplate") + costMoney:SetPoint("RIGHT", row, "RIGHT", -2, 0) + costMoney:SetWidth(100) + costMoney:SetHeight(14) + costMoney:SetFrameLevel(row:GetFrameLevel() + 2) + costMoney:SetScale(0.85) + costMoney:UnregisterAllEvents() + costMoney:SetScript("OnEvent", nil) + costMoney:SetScript("OnShow", nil) + costMoney.moneyType = nil + costMoney.hasPickup = nil + costMoney.small = 1 + row.costMoney = costMoney + + local catFS = row:CreateFontString(nil, "OVERLAY") + catFS:SetFont(GetFont(), 11, "OUTLINE") + catFS:SetPoint("LEFT", row, "LEFT", 4, 0) + catFS:SetJustifyH("LEFT") + catFS:SetTextColor(T.catHeader[1], T.catHeader[2], T.catHeader[3]) + catFS:Hide() + row.catFS = catFS + + local catSep = row:CreateTexture(nil, "ARTWORK") + catSep:SetTexture("Interface\\Buttons\\WHITE8X8") + catSep:SetHeight(1) + catSep:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) + catSep:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0) + catSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) + catSep:Hide() + row.catSep = catSep + + local highlight = row:CreateTexture(nil, "HIGHLIGHT") + highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + highlight:SetBlendMode("ADD") + highlight:SetAllPoints(row) + highlight:SetAlpha(0.3) + row.highlight = highlight + + local selectedBg = row:CreateTexture(nil, "ARTWORK") + selectedBg:SetTexture("Interface\\Buttons\\WHITE8X8") + selectedBg:SetAllPoints(row) + selectedBg:SetVertexColor(T.selectedRowBg[1], T.selectedRowBg[2], T.selectedRowBg[3], 0.40) + selectedBg:Hide() + row.selectedBg = selectedBg + + local selectedGlow = row:CreateTexture(nil, "ARTWORK") + selectedGlow:SetTexture("Interface\\Buttons\\WHITE8X8") + selectedGlow:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0) + selectedGlow:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) + selectedGlow:SetWidth(4) + selectedGlow:SetVertexColor(1, 0.65, 0.85, 1) + selectedGlow:Hide() + row.selectedGlow = selectedGlow + + local selTop = row:CreateTexture(nil, "OVERLAY") + selTop:SetTexture("Interface\\Buttons\\WHITE8X8") + selTop:SetHeight(1) + selTop:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0) + selTop:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 0) + selTop:SetVertexColor(T.selectedRowBorder[1], T.selectedRowBorder[2], T.selectedRowBorder[3], T.selectedRowBorder[4]) + selTop:Hide() + row.selTop = selTop + + local selBot = row:CreateTexture(nil, "OVERLAY") + selBot:SetTexture("Interface\\Buttons\\WHITE8X8") + selBot:SetHeight(1) + selBot:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) + selBot:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0) + selBot:SetVertexColor(T.selectedRowBorder[1], T.selectedRowBorder[2], T.selectedRowBorder[3], T.selectedRowBorder[4]) + selBot:Hide() + row.selBot = selBot + + row.serviceIndex = nil + row.isHeader = false + + row:SetScript("OnEnter", function() + if this.serviceIndex and not this.isHeader then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + local ok = pcall(GameTooltip.SetTrainerService, GameTooltip, this.serviceIndex) + if ok then + GameTooltip:Show() + else + GameTooltip:Hide() + end + end + end) + row:SetScript("OnLeave", function() GameTooltip:Hide() end) + + function row:SetAsHeader(name, collapsed) + self.isHeader = true + self:SetHeight(CAT_ROW_H) + self.iconFrame:Hide() + self.qualGlow:Hide() + self.nameFS:Hide() + self.subFS:Hide() + self.costFS:Hide() + self.costMoney:Hide() + self.selectedBg:Hide() + self.selectedGlow:Hide() + self.selTop:Hide() + self.selBot:Hide() + self.highlight:SetAlpha(0.15) + local arrow = collapsed and "+" or "-" + self.catFS:SetText(arrow .. " " .. (name or "")) + self.catFS:Show() + self.catSep:Show() + end + + function row:SetAsService(svc) + self.isHeader = false + self.serviceIndex = svc.index + self:SetHeight(LIST_ROW_H) + self.iconFrame:Show() + self.nameFS:Show() + self.subFS:Show() + self.costFS:Hide() + self.costMoney:Show() + self.catFS:Hide() + self.catSep:Hide() + self.highlight:SetAlpha(0.3) + + local iconTex = GetTrainerServiceIcon and GetTrainerServiceIcon(svc.index) + self.icon:SetTexture(iconTex) + self.nameFS:SetText(svc.name) + self.subFS:SetText(svc.subText) + + if svc.category == "available" then + self.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3]) + self.icon:SetVertexColor(1, 1, 1) + elseif svc.category == "unavailable" then + self.nameFS:SetTextColor(T.unavailable[1], T.unavailable[2], T.unavailable[3]) + self.icon:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) + else + self.nameFS:SetTextColor(T.used[1], T.used[2], T.used[3]) + self.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3]) + end + + local qc = QUALITY_COLORS[svc.quality] + if qc and svc.quality and svc.quality >= 2 then + self.qualGlow:SetVertexColor(qc[1], qc[2], qc[3]) + self.qualGlow:Show() + self.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1) + else + self.qualGlow:Hide() + self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + end + + local ok, cost = pcall(GetTrainerServiceCost, svc.index) + if ok and cost and cost > 0 then + SetMoneyFrame(self.costMoney, cost) + else + SetMoneyFrame(self.costMoney, 0) + end + end + + function row:Clear() + self.serviceIndex = nil + self.isHeader = false + self.qualGlow:Hide() + self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + self.selectedBg:Hide() + self.selectedGlow:Hide() + self.selTop:Hide() + self.selBot:Hide() + self.costMoney:Hide() + self:Hide() + end + + return row +end + +-------------------------------------------------------------------------------- +-- Build Display List (with categories) +-------------------------------------------------------------------------------- +local function BuildDisplayList() + displayList = {} + local numServices = GetNumTrainerServices and GetNumTrainerServices() or 0 + if numServices == 0 then return end + + local currentCat = nil + local catServices = {} + local categories = {} + local catOrder = {} + + for i = 1, numServices do + local name, subText, category = GetTrainerServiceInfo(i) + if name then + local isHdr = IsServiceHeader(i) + if isHdr then + currentCat = name + if not catServices[name] then + catServices[name] = {} + table.insert(catOrder, name) + end + else + if not currentCat then + currentCat = "技能" + if not catServices[currentCat] then + catServices[currentCat] = {} + table.insert(catOrder, currentCat) + end + end + local show = false + if currentFilter == "all" then + show = true + elseif currentFilter == (category or "") then + show = true + end + if show then + table.insert(catServices[currentCat], { + index = i, + name = name, + subText = subText or "", + category = category or "unavailable", + quality = GetServiceQuality(i), + }) + end + end + end + end + + local hasCats = table.getn(catOrder) > 1 + + for _, catName in ipairs(catOrder) do + local svcs = catServices[catName] + if table.getn(svcs) > 0 then + if hasCats then + table.insert(displayList, { + type = "header", + name = catName, + collapsed = collapsedCats[catName], + }) + end + if not collapsedCats[catName] then + for _, svc in ipairs(svcs) do + table.insert(displayList, { + type = "service", + data = svc, + }) + end + end + end + end + + if table.getn(catOrder) <= 1 and table.getn(displayList) == 0 then + local allCat = catOrder[1] or "技能" + local svcs = catServices[allCat] or {} + for _, svc in ipairs(svcs) do + table.insert(displayList, { type = "service", data = svc }) + end + end +end + +-------------------------------------------------------------------------------- +-- Update Functions +-------------------------------------------------------------------------------- +local function UpdateList() + if not MainFrame or not MainFrame:IsVisible() then return end + + BuildDisplayList() + + local content = MainFrame.listScroll.content + local count = table.getn(displayList) + local y = 0 + + for i = 1, MAX_ROWS do + local row = rowButtons[i] + if i <= count then + local entry = displayList[i] + row:ClearAllPoints() + + if entry.type == "header" then + row:SetAsHeader(entry.name, entry.collapsed) + row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + row.catName = entry.name + row:Show() + y = y + CAT_ROW_H + else + row:SetAsService(entry.data) + row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + row.catName = nil + row:Show() + y = y + LIST_ROW_H + + if selectedIndex == entry.data.index then + row.iconFrame:SetBackdropBorderColor(1, 0.65, 0.85, 1) + row.iconFrame:SetBackdropColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.5) + row.selectedBg:Show() + row.selectedGlow:Show() + row.selTop:Show() + row.selBot:Show() + row.nameFS:SetTextColor(T.selectedNameText[1], T.selectedNameText[2], T.selectedNameText[3]) + else + row.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + row.iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + row.selectedBg:Hide() + row.selectedGlow:Hide() + row.selTop:Hide() + row.selBot:Hide() + end + end + else + row:Clear() + end + end + + content:SetHeight(math.max(1, y)) +end + +local function UpdateDetail() + if not MainFrame then return end + local detail = MainFrame.detail + + if not selectedIndex then + detail.iconFrame:Hide() + detail.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + detail.nameFS:SetText("") + detail.subFS:SetText("") + detail.reqFS:SetText("") + detail.infoFS:SetText("") + detail.descFS:SetText("") + detail.descDivider:Hide() + MainFrame.trainBtn:SetDisabled(true) + SetMoneyFrame(MainFrame.costMoney, 0) + SetMoneyFrame(MainFrame.availMoney, 0) + MainFrame.costLabel:SetText("花费:") + return + end + + local name, subText, category = GetTrainerServiceInfo(selectedIndex) + local iconTex = GetTrainerServiceIcon and GetTrainerServiceIcon(selectedIndex) + local ok, cost = pcall(GetTrainerServiceCost, selectedIndex) + if not ok then cost = 0 end + local levelReq = 0 + if GetTrainerServiceLevelReq then + local ok2, lr = pcall(GetTrainerServiceLevelReq, selectedIndex) + if ok2 then levelReq = lr or 0 end + end + + detail.icon:SetTexture(iconTex) + detail.iconFrame:Show() + + local quality = GetServiceQuality(selectedIndex) + local qc = QUALITY_COLORS[quality] + if qc and quality and quality >= 2 then + detail.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1) + else + detail.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + end + + detail.nameFS:SetText(name or "") + + if category == "available" then + detail.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3]) + elseif category == "unavailable" then + detail.nameFS:SetTextColor(T.unavailable[1], T.unavailable[2], T.unavailable[3]) + else + detail.nameFS:SetTextColor(T.used[1], T.used[2], T.used[3]) + end + + detail.subFS:SetText(subText or "") + + local reqParts = {} + if levelReq and levelReq > 0 then + local pLvl = UnitLevel("player") or 60 + local color = pLvl >= levelReq and "|cff40ff40" or "|cffff4040" + table.insert(reqParts, color .. "需要等级 " .. levelReq .. "|r") + end + if GetTrainerServiceSkillReq then + local ok3, skillName, skillRank, hasReq = pcall(GetTrainerServiceSkillReq, selectedIndex) + if ok3 and skillName and skillName ~= "" then + local color = hasReq and "|cff40ff40" or "|cffff4040" + table.insert(reqParts, color .. "需要 " .. skillName .. " (" .. (skillRank or 0) .. ")|r") + end + end + detail.reqFS:SetText(table.concat(reqParts, " ")) + + local spellInfo, descText = GetServiceTooltipInfo(selectedIndex) + detail.infoFS:SetText(spellInfo) + detail.descFS:SetText(descText) + detail.descDivider:Show() + + local textH = detail.descFS:GetHeight() or 40 + detail.descScroll:GetScrollChild():SetHeight(math.max(1, textH)) + detail.descScroll:SetVerticalScroll(0) + + local canTrain = (category == "available") and cost and (GetMoney() >= cost) + MainFrame.trainBtn:SetDisabled(not canTrain) + + if cost and cost > 0 then + MainFrame.costLabel:SetText("花费:") + SetMoneyFrame(MainFrame.costMoney, cost) + else + MainFrame.costLabel:SetText("花费: 免费") + SetMoneyFrame(MainFrame.costMoney, 0) + end + SetMoneyFrame(MainFrame.availMoney, GetMoney()) +end + +local function UpdateFilters() + if not MainFrame then return end + MainFrame.filterAll:SetActive(currentFilter == "all") + MainFrame.filterAvail:SetActive(currentFilter == "available") + MainFrame.filterUnavail:SetActive(currentFilter == "unavailable") + MainFrame.filterUsed:SetActive(currentFilter == "used") +end + +local function FullUpdate() + UpdateFilters() + UpdateList() + UpdateDetail() +end + +local function SelectService(index) + selectedIndex = index + local ok = pcall(SelectTrainerService, index) + FullUpdate() +end + +local function ToggleCategory(catName) + if collapsedCats[catName] then + collapsedCats[catName] = nil + else + collapsedCats[catName] = true + end + FullUpdate() +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function TUI:Initialize() + if MainFrame then return end + + MainFrame = CreateFrame("Frame", "SFramesTrainerFrame", UIParent) + MainFrame:SetWidth(FRAME_W) + MainFrame:SetHeight(FRAME_H) + MainFrame:SetPoint("LEFT", UIParent, "LEFT", 64, 0) + MainFrame:SetFrameStrata("HIGH") + MainFrame:SetToplevel(true) + MainFrame:EnableMouse(true) + MainFrame:SetMovable(true) + MainFrame:RegisterForDrag("LeftButton") + MainFrame:SetScript("OnDragStart", function() this:StartMoving() end) + MainFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + SetRoundBackdrop(MainFrame) + CreateShadow(MainFrame) + + -- Header + local header = CreateFrame("Frame", nil, MainFrame) + header:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 0, 0) + header:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 0, 0) + header:SetHeight(HEADER_H) + header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) + header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) + + local titleIco = SFrames:CreateIcon(header, "spellbook", 16) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0) + titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) + + local npcNameFS = header:CreateFontString(nil, "OVERLAY") + npcNameFS:SetFont(GetFont(), 14, "OUTLINE") + npcNameFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) + npcNameFS:SetPoint("RIGHT", header, "RIGHT", -30, 0) + npcNameFS:SetJustifyH("LEFT") + npcNameFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + MainFrame.npcNameFS = npcNameFS + + local closeBtn = CreateFrame("Button", nil, header) + closeBtn:SetWidth(20); closeBtn:SetHeight(20) + closeBtn:SetPoint("TOPRIGHT", header, "TOPRIGHT", -8, -6) + local closeTex = closeBtn:CreateTexture(nil, "ARTWORK") + closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") + closeTex:SetTexCoord(0.25, 0.375, 0, 0.125) + closeTex:SetAllPoints() + closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) + closeBtn:SetScript("OnClick", function() MainFrame:Hide() end) + closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end) + closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) + + local headerSep = MainFrame:CreateTexture(nil, "ARTWORK") + headerSep:SetTexture("Interface\\Buttons\\WHITE8X8") + headerSep:SetHeight(1) + headerSep:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 6, -HEADER_H) + headerSep:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -6, -HEADER_H) + headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + -- Filter bar + local filterBar = CreateFrame("Frame", nil, MainFrame) + filterBar:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -(HEADER_H + 4)) + filterBar:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -SIDE_PAD, -(HEADER_H + 4)) + filterBar:SetHeight(FILTER_H) + + local fAll = CreateFilterBtn(filterBar, "全部", 52) + fAll:SetPoint("LEFT", filterBar, "LEFT", 0, 0) + fAll:SetScript("OnClick", function() currentFilter = "all"; FullUpdate() end) + MainFrame.filterAll = fAll + + local fAvail = CreateFilterBtn(filterBar, "可学习", 60) + fAvail:SetPoint("LEFT", fAll, "RIGHT", 4, 0) + fAvail:SetScript("OnClick", function() currentFilter = "available"; FullUpdate() end) + MainFrame.filterAvail = fAvail + + local fUnavail = CreateFilterBtn(filterBar, "不可学", 60) + fUnavail:SetPoint("LEFT", fAvail, "RIGHT", 4, 0) + fUnavail:SetScript("OnClick", function() currentFilter = "unavailable"; FullUpdate() end) + MainFrame.filterUnavail = fUnavail + + local fUsed = CreateFilterBtn(filterBar, "已学会", 60) + fUsed:SetPoint("LEFT", fUnavail, "RIGHT", 4, 0) + fUsed:SetScript("OnClick", function() currentFilter = "used"; FullUpdate() end) + MainFrame.filterUsed = fUsed + + -- Scrollable list area + local listTop = HEADER_H + FILTER_H + 8 + local listBottom = DETAIL_H + BOTTOM_H + 8 + + local listScroll = CreateFrame("ScrollFrame", "SFramesTrainerListScroll", MainFrame) + listScroll:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -listTop) + listScroll:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, listBottom) + + local listContent = CreateFrame("Frame", "SFramesTrainerListContent", listScroll) + listContent:SetWidth(CONTENT_W) + listContent:SetHeight(1) + listScroll:SetScrollChild(listContent) + + listScroll:EnableMouseWheel(true) + listScroll:SetScript("OnMouseWheel", function() + local cur = this:GetVerticalScroll() + local maxVal = this:GetVerticalScrollRange() + if arg1 > 0 then + this:SetVerticalScroll(math.max(0, cur - SCROLL_STEP)) + else + this:SetVerticalScroll(math.min(maxVal, cur + SCROLL_STEP)) + end + end) + listScroll.content = listContent + MainFrame.listScroll = listScroll + + for i = 1, MAX_ROWS do + local row = CreateListRow(listContent, i) + row:SetScript("OnClick", function() + if this.isHeader and this.catName then + ToggleCategory(this.catName) + elseif this.serviceIndex then + SelectService(this.serviceIndex) + end + end) + rowButtons[i] = row + end + + -- Detail separator + local detailSep = MainFrame:CreateTexture(nil, "ARTWORK") + detailSep:SetTexture("Interface\\Buttons\\WHITE8X8") + detailSep:SetHeight(1) + detailSep:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", 6, DETAIL_H + BOTTOM_H + 4) + detailSep:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -6, DETAIL_H + BOTTOM_H + 4) + detailSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + -- Detail area + local detail = CreateFrame("Frame", nil, MainFrame) + detail:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, BOTTOM_H + 4) + detail:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, BOTTOM_H + 4) + detail:SetHeight(DETAIL_H) + MainFrame.detail = detail + + local dIconFrame = CreateFrame("Frame", nil, detail) + dIconFrame:SetWidth(42); dIconFrame:SetHeight(42) + dIconFrame:SetPoint("TOPLEFT", detail, "TOPLEFT", 2, -6) + dIconFrame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + dIconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + dIconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + dIconFrame:Hide() + detail.iconFrame = dIconFrame + + local dIcon = dIconFrame:CreateTexture(nil, "ARTWORK") + dIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + dIcon:SetPoint("TOPLEFT", dIconFrame, "TOPLEFT", 3, -3) + dIcon:SetPoint("BOTTOMRIGHT", dIconFrame, "BOTTOMRIGHT", -3, 3) + detail.icon = dIcon + + local dNameFS = detail:CreateFontString(nil, "OVERLAY") + dNameFS:SetFont(GetFont(), 13, "OUTLINE") + dNameFS:SetPoint("TOPLEFT", dIconFrame, "TOPRIGHT", 8, -2) + dNameFS:SetPoint("RIGHT", detail, "RIGHT", -4, 0) + dNameFS:SetJustifyH("LEFT") + dNameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + detail.nameFS = dNameFS + + local dSubFS = detail:CreateFontString(nil, "OVERLAY") + dSubFS:SetFont(GetFont(), 11, "OUTLINE") + dSubFS:SetPoint("TOPLEFT", dNameFS, "BOTTOMLEFT", 0, -2) + dSubFS:SetJustifyH("LEFT") + dSubFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + detail.subFS = dSubFS + + local dReqFS = detail:CreateFontString(nil, "OVERLAY") + dReqFS:SetFont(GetFont(), 11, "OUTLINE") + dReqFS:SetPoint("TOPLEFT", dIconFrame, "BOTTOMLEFT", 0, -6) + dReqFS:SetPoint("RIGHT", detail, "RIGHT", -4, 0) + dReqFS:SetJustifyH("LEFT") + detail.reqFS = dReqFS + + local dInfoFS = detail:CreateFontString(nil, "OVERLAY") + dInfoFS:SetFont(GetFont(), 11) + dInfoFS:SetPoint("TOPLEFT", dReqFS, "BOTTOMLEFT", 0, -4) + dInfoFS:SetPoint("RIGHT", detail, "RIGHT", -4, 0) + dInfoFS:SetJustifyH("LEFT") + dInfoFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + detail.infoFS = dInfoFS + + local descDivider = detail:CreateTexture(nil, "ARTWORK") + descDivider:SetTexture("Interface\\Buttons\\WHITE8X8") + descDivider:SetHeight(1) + descDivider:SetPoint("TOPLEFT", dInfoFS, "BOTTOMLEFT", 0, -5) + descDivider:SetPoint("RIGHT", detail, "RIGHT", -4, 0) + descDivider:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) + detail.descDivider = descDivider + + local descScroll = CreateFrame("ScrollFrame", nil, detail) + descScroll:SetPoint("TOPLEFT", descDivider, "BOTTOMLEFT", 0, -4) + descScroll:SetPoint("BOTTOMRIGHT", detail, "BOTTOMRIGHT", -4, 2) + descScroll:EnableMouseWheel(true) + descScroll:SetScript("OnMouseWheel", function() + local cur = this:GetVerticalScroll() + local maxVal = this:GetVerticalScrollRange() + if arg1 > 0 then + this:SetVerticalScroll(math.max(0, cur - 14)) + else + this:SetVerticalScroll(math.min(maxVal, cur + 14)) + end + end) + detail.descScroll = descScroll + + local descContent = CreateFrame("Frame", nil, descScroll) + descContent:SetWidth(CONTENT_W - 8) + descContent:SetHeight(1) + descScroll:SetScrollChild(descContent) + + local dDescFS = descContent:CreateFontString(nil, "OVERLAY") + dDescFS:SetFont(GetFont(), 11) + dDescFS:SetPoint("TOPLEFT", descContent, "TOPLEFT", 0, 0) + dDescFS:SetWidth(CONTENT_W - 8) + dDescFS:SetJustifyH("LEFT") + dDescFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + detail.descFS = dDescFS + + -- Bottom bar + local bottomSep = MainFrame:CreateTexture(nil, "ARTWORK") + bottomSep:SetTexture("Interface\\Buttons\\WHITE8X8") + bottomSep:SetHeight(1) + bottomSep:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", 6, BOTTOM_H) + bottomSep:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -6, BOTTOM_H) + bottomSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + local costLabel = MainFrame:CreateFontString(nil, "OVERLAY") + costLabel:SetFont(GetFont(), 11, "OUTLINE") + costLabel:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, 32) + costLabel:SetJustifyH("LEFT") + costLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + costLabel:SetText("花费:") + MainFrame.costLabel = costLabel + + local costMoney = CreateFrame("Frame", "SFramesTrainerCostMoney", MainFrame, "SmallMoneyFrameTemplate") + costMoney:SetPoint("LEFT", costLabel, "RIGHT", 4, 0) + costMoney:SetWidth(120) + costMoney:SetHeight(14) + costMoney:SetFrameLevel(MainFrame:GetFrameLevel() + 5) + costMoney:UnregisterAllEvents() + costMoney:SetScript("OnEvent", nil) + costMoney:SetScript("OnShow", nil) + costMoney.moneyType = nil + costMoney.small = 1 + MainFrame.costMoney = costMoney + + local availLabel = MainFrame:CreateFontString(nil, "OVERLAY") + availLabel:SetFont(GetFont(), 11, "OUTLINE") + availLabel:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, 16) + availLabel:SetJustifyH("LEFT") + availLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + availLabel:SetText("可用:") + MainFrame.availLabel = availLabel + + local availMoney = CreateFrame("Frame", "SFramesTrainerAvailMoney", MainFrame, "SmallMoneyFrameTemplate") + availMoney:SetPoint("LEFT", availLabel, "RIGHT", 4, 0) + availMoney:SetWidth(120) + availMoney:SetHeight(14) + availMoney:SetFrameLevel(MainFrame:GetFrameLevel() + 5) + availMoney:UnregisterAllEvents() + availMoney:SetScript("OnEvent", nil) + availMoney:SetScript("OnShow", nil) + availMoney.moneyType = nil + availMoney.small = 1 + MainFrame.availMoney = availMoney + + local trainBtn = CreateActionBtn(MainFrame, "训练", 80) + trainBtn:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -(SIDE_PAD + 90), 8) + trainBtn:SetScript("OnClick", function() + if this.disabled then return end + if IsControlKeyDown() and BuyTrainerService then + local numServices = GetNumTrainerServices and GetNumTrainerServices() or 0 + local gold = GetMoney() + for i = 1, numServices do + local name, _, category = GetTrainerServiceInfo(i) + if name and category == "available" and not IsServiceHeader(i) then + local ok, cost = pcall(GetTrainerServiceCost, i) + if ok and cost and gold >= cost then + pcall(BuyTrainerService, i) + gold = gold - cost + end + end + end + return + end + if selectedIndex and BuyTrainerService then + BuyTrainerService(selectedIndex) + end + end) + trainBtn:SetScript("OnEnter", function() + if not this.disabled then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:AddLine("Ctrl+点击: 学习所有可学技能", 0.8, 0.8, 0.8) + GameTooltip:Show() + end) + trainBtn:SetScript("OnLeave", function() + if not this.disabled then + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + GameTooltip:Hide() + end) + MainFrame.trainBtn = trainBtn + + local exitBtn = CreateActionBtn(MainFrame, "退出", 80) + exitBtn:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, 8) + exitBtn:SetScript("OnClick", function() MainFrame:Hide() end) + + -- Events + local function CleanupBlizzardTrainer() + if not ClassTrainerFrame then return end + ClassTrainerFrame:SetScript("OnHide", function() end) + if HideUIPanel then pcall(HideUIPanel, ClassTrainerFrame) end + if ClassTrainerFrame:IsVisible() then ClassTrainerFrame:Hide() end + ClassTrainerFrame:SetAlpha(0) + ClassTrainerFrame:EnableMouse(false) + end + + MainFrame:SetScript("OnHide", function() + if CloseTrainer then pcall(CloseTrainer) end + if CloseGossip then pcall(CloseGossip) end + CleanupBlizzardTrainer() + end) + + MainFrame:RegisterEvent("TRAINER_SHOW") + MainFrame:RegisterEvent("TRAINER_UPDATE") + MainFrame:RegisterEvent("TRAINER_CLOSED") + MainFrame:SetScript("OnEvent", function() + if event == "TRAINER_SHOW" then + if ClassTrainerFrame then + ClassTrainerFrame:SetScript("OnHide", function() end) + ClassTrainerFrame:SetAlpha(0) + ClassTrainerFrame:EnableMouse(false) + end + + selectedIndex = nil + currentFilter = "all" + collapsedCats = {} + local npcName = UnitName("npc") or "训练师" + if IsTradeskillTrainer and IsTradeskillTrainer() then + npcName = npcName .. " - 专业训练" + end + MainFrame.npcNameFS:SetText(npcName) + MainFrame:Show() + FullUpdate() + for _, entry in ipairs(displayList) do + if entry.type == "service" then + SelectService(entry.data.index) + break + end + end + + MainFrame._hideBlizzTimer = 0 + MainFrame:SetScript("OnUpdate", function() + if not this._hideBlizzTimer then return end + this._hideBlizzTimer = this._hideBlizzTimer + arg1 + if this._hideBlizzTimer > 0.05 then + this._hideBlizzTimer = nil + this:SetScript("OnUpdate", nil) + CleanupBlizzardTrainer() + end + end) + elseif event == "TRAINER_UPDATE" then + if MainFrame:IsVisible() then FullUpdate() end + elseif event == "TRAINER_CLOSED" then + MainFrame._hideBlizzTimer = nil + MainFrame:SetScript("OnUpdate", nil) + CleanupBlizzardTrainer() + MainFrame:Hide() + end + end) + MainFrame:Hide() + + tinsert(UISpecialFrames, "SFramesTrainerFrame") +end + +-------------------------------------------------------------------------------- +-- Bootstrap +-------------------------------------------------------------------------------- +local bootstrap = CreateFrame("Frame") +bootstrap:RegisterEvent("PLAYER_LOGIN") +bootstrap:RegisterEvent("ADDON_LOADED") +bootstrap:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + if SFramesDB.enableTrainer == nil then + SFramesDB.enableTrainer = true + end + if SFramesDB.enableTrainer ~= false then + TUI:Initialize() + end + elseif event == "ADDON_LOADED" and arg1 == "Blizzard_TrainerUI" then + if MainFrame and ClassTrainerFrame then + ClassTrainerFrame:SetScript("OnHide", function() end) + ClassTrainerFrame:SetAlpha(0) + ClassTrainerFrame:EnableMouse(false) + end + end +end) diff --git a/Tweaks.lua b/Tweaks.lua new file mode 100644 index 0000000..c67d7eb --- /dev/null +++ b/Tweaks.lua @@ -0,0 +1,1051 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Tweaks -- Ported from ShaguTweaks +-- 1. Auto Stance - auto switch warrior/druid stance on spell cast +-- 2. SuperWoW - GUID-based cast/channel data for SuperWoW client +-- 3. Turtle Compat - hide TW's overlapping target HP text, etc. +-- 4. Cooldown Numbers - show remaining cooldown time as text overlay +-- 5. Dark UI - darken the entire interface +-- 6. WorldMap Window - turn fullscreen map into a movable/scalable window +-- 7. Auto Dismount - cancel shapeshift/mount when casting incompatible spells +-------------------------------------------------------------------------------- + +SFrames.Tweaks = SFrames.Tweaks or {} +local Tweaks = SFrames.Tweaks + +SFrames.castdb = SFrames.castdb or {} + +local function GetTweaksCfg() + if not SFramesDB or type(SFramesDB.Tweaks) ~= "table" then + return { autoStance = true, superWoW = true, turtleCompat = true, + cooldownNumbers = true, darkUI = false, worldMapWindow = false } + end + return SFramesDB.Tweaks +end + +local function strsplit(delimiter, str) + local result = {} + local pattern = "([^" .. delimiter .. "]+)" + for match in string.gfind(str, pattern) do + table.insert(result, match) + end + return unpack(result) +end + +-- hooksecurefunc polyfill for WoW 1.12 (vanilla) +local _hooks = {} +local _hooksecurefunc = hooksecurefunc +if not _hooksecurefunc then + _hooksecurefunc = function(tbl, name, func) + if type(tbl) == "string" then + func = name + name = tbl + tbl = getfenv(0) + end + if not tbl or not tbl[name] then return end + local key = tostring(func) + _hooks[key] = {} + _hooks[key].old = tbl[name] + _hooks[key].new = func + tbl[name] = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) + local r1,r2,r3,r4,r5 = _hooks[key].old(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) + pcall(_hooks[key].new, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) + return r1,r2,r3,r4,r5 + end + end +end + +-------------------------------------------------------------------------------- +-- Auto Stance +-- When a spell fails because you're in the wrong stance/form, automatically +-- switch to the required one. Works for warriors, druids, etc. +-------------------------------------------------------------------------------- +local function InitAutoStance() + local frame = CreateFrame("Frame", "NanamiAutoStance") + local scanString = string.gsub(SPELL_FAILED_ONLY_SHAPESHIFT, "%%s", "(.+)") + + frame:RegisterEvent("UI_ERROR_MESSAGE") + frame:SetScript("OnEvent", function() + for stances in string.gfind(arg1, scanString) do + for _, stance in pairs({ strsplit(",", stances) }) do + CastSpellByName(string.gsub(stance, "^%s*(.-)%s*$", "%1")) + end + end + end) +end + +-------------------------------------------------------------------------------- +-- Auto Dismount / Cancel Shapeshift +-- When casting a spell that fails because you are mounted or shapeshifted, +-- automatically cancel the mount/shapeshift buff so the next cast succeeds. +-------------------------------------------------------------------------------- +local function InitAutoDismount() + local dismount = CreateFrame("Frame", "NanamiAutoDismount") + + local scanner = CreateFrame("GameTooltip", "NanamiDismountScan", nil, "GameTooltipTemplate") + scanner:SetOwner(WorldFrame, "ANCHOR_NONE") + + local mountStrings = { + "^Increases speed by (.+)%%", + "^Erhöht Tempo um (.+)%%", + "^Aumenta la velocidad en un (.+)%%", + "^Augmente la vitesse de (.+)%%", + "^Скорость увеличена на (.+)%%", + "^이동 속도 (.+)%%만큼 증가", + "^速度提高(.+)%%", + "speed based on", "Slow and steady...", "Riding", + "Lento y constante...", "Aumenta la velocidad según tu habilidad de Montar.", + "根据您的骑行技能提高速度。", "根据骑术技能提高速度。", "又慢又稳......", + } + + local shapeshiftIcons = { + "ability_racial_bearform", "ability_druid_catform", + "ability_druid_travelform", "spell_nature_forceofnature", + "ability_druid_aquaticform", "spell_nature_spiritwolf", + "ability_druid_treeoflife", "ability_druid_stagform", + } + + local errorStrings = {} + local errorGlobals = { + "SPELL_FAILED_NOT_MOUNTED", "ERR_ATTACK_MOUNTED", "ERR_TAXIPLAYERALREADYMOUNTED", + "SPELL_FAILED_NOT_SHAPESHIFT", "SPELL_FAILED_NO_ITEMS_WHILE_SHAPESHIFTED", + "SPELL_NOT_SHAPESHIFTED", "SPELL_NOT_SHAPESHIFTED_NOSPACE", + "ERR_CANT_INTERACT_SHAPESHIFTED", "ERR_NOT_WHILE_SHAPESHIFTED", + "ERR_NO_ITEMS_WHILE_SHAPESHIFTED", "ERR_TAXIPLAYERSHAPESHIFTED", + "ERR_MOUNT_SHAPESHIFTED", + } + for _, name in pairs(errorGlobals) do + local val = getfenv(0)[name] + if val then table.insert(errorStrings, val) end + end + + dismount:RegisterEvent("UI_ERROR_MESSAGE") + dismount:SetScript("OnEvent", function() + if arg1 == SPELL_FAILED_NOT_STANDING then + SitOrStand() + return + end + + local matched = false + for _, err in pairs(errorStrings) do + if arg1 == err then matched = true break end + end + if not matched then return end + + for i = 0, 31 do + scanner:ClearLines() + scanner:SetPlayerBuff(i) + for line = 1, scanner:NumLines() do + local text = getfenv(0)["NanamiDismountScanTextLeft" .. line] + if text and text:GetText() then + for _, str in pairs(mountStrings) do + if string.find(text:GetText(), str) then + CancelPlayerBuff(i) + return + end + end + end + end + + local buff = GetPlayerBuffTexture(i) + if buff then + for _, icon in pairs(shapeshiftIcons) do + if string.find(string.lower(buff), icon) then + CancelPlayerBuff(i) + return + end + end + end + end + end) +end + +-------------------------------------------------------------------------------- +-- SuperWoW Compatibility +-- Provides GUID-based cast/channel data when SuperWoW client mod is active. +-- Data stored in SFrames.castdb[guid] for consumption by castbar features. +-------------------------------------------------------------------------------- +local function InitSuperWoW() + if not GetPlayerBuffID or not CombatLogAdd or not SpellInfo then return end + + local castdb = SFrames.castdb + + local frame = CreateFrame("Frame", "NanamiSuperWoW") + frame:RegisterEvent("UNIT_CASTEVENT") + frame:SetScript("OnEvent", function() + if arg3 == "START" or arg3 == "CAST" or arg3 == "CHANNEL" then + local guid = arg1 + local spell, icon, _ + if SpellInfo and SpellInfo(arg4) then + spell, _, icon = SpellInfo(arg4) + end + spell = spell or UNKNOWN + icon = icon or "Interface\\Icons\\INV_Misc_QuestionMark" + + if not castdb[guid] then castdb[guid] = {} end + castdb[guid].cast = spell + castdb[guid].rank = nil + castdb[guid].start = GetTime() + castdb[guid].casttime = arg5 + castdb[guid].icon = icon + castdb[guid].channel = (arg3 == "CHANNEL") or false + + SFrames.superwow_active = true + elseif arg3 == "FAIL" then + local guid = arg1 + if castdb[guid] then + castdb[guid].cast = nil + castdb[guid].rank = nil + castdb[guid].start = nil + castdb[guid].casttime = nil + castdb[guid].icon = nil + castdb[guid].channel = nil + end + end + end) +end + +-------------------------------------------------------------------------------- +-- Turtle WoW Compatibility +-- Hides TW's built-in target HP text that overlaps with Nanami-UI frames, +-- and applies other TW-specific fixes. +-------------------------------------------------------------------------------- +local function ApplyWorldMapWindowLayout() + WorldMapFrame:SetMovable(true) + WorldMapFrame:EnableMouse(true) + WorldMapFrame:SetScale(.85) + WorldMapFrame:ClearAllPoints() + WorldMapFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 30) + WorldMapFrame:SetWidth(WorldMapButton:GetWidth() + 15) + WorldMapFrame:SetHeight(WorldMapButton:GetHeight() + 55) + if WorldMapFrameTitle then + WorldMapFrameTitle:SetPoint("TOP", WorldMapFrame, 0, 17) + end + BlackoutWorld:Hide() +end + +local function InitTurtleCompat() + if not TargetHPText or not TargetHPPercText then return end + + TargetHPText:Hide() + TargetHPText.Show = function() return end + + TargetHPPercText:Hide() + TargetHPPercText.Show = function() return end + + if WorldMapFrame_Maximize then + local origMaximize = WorldMapFrame_Maximize + WorldMapFrame_Maximize = function() + origMaximize() + local cfg = GetTweaksCfg() + if cfg.worldMapWindow ~= false then + ApplyWorldMapWindowLayout() + elseif WorldMapFrameTitle then + WorldMapFrameTitle:SetPoint("TOP", WorldMapFrame, 0, 17) + end + end + end +end + +-------------------------------------------------------------------------------- +-- WorldMap Window +-- Turn the fullscreen world map into a movable, scalable window. +-- Ctrl+Scroll to zoom, Shift+Scroll to change transparency, drag to move. +-------------------------------------------------------------------------------- +local function HookScript(frame, script, func) + local prev = frame:GetScript(script) + frame:SetScript(script, function(a1,a2,a3,a4,a5,a6,a7,a8,a9) + if prev then prev(a1,a2,a3,a4,a5,a6,a7,a8,a9) end + func(a1,a2,a3,a4,a5,a6,a7,a8,a9) + end) +end + +local function InitWorldMapWindow() + if Cartographer or METAMAP_TITLE then return end + + table.insert(UISpecialFrames, "WorldMapFrame") + + local _G = getfenv(0) + _G.ToggleWorldMap = function() + if WorldMapFrame:IsShown() then + WorldMapFrame:Hide() + else + WorldMapFrame:Show() + end + end + + UIPanelWindows["WorldMapFrame"] = { area = "center" } + + HookScript(WorldMapFrame, "OnShow", function() + this:EnableKeyboard(false) + this:EnableMouseWheel(1) + WorldMapFrame:SetScale(.85) + WorldMapFrame:SetAlpha(1) + WorldMapFrame:SetFrameStrata("FULLSCREEN_DIALOG") + end) + + HookScript(WorldMapFrame, "OnMouseWheel", function() + if IsShiftKeyDown() then + local newAlpha = WorldMapFrame:GetAlpha() + arg1 / 10 + if newAlpha < 0.2 then newAlpha = 0.2 end + if newAlpha > 1 then newAlpha = 1 end + WorldMapFrame:SetAlpha(newAlpha) + elseif IsControlKeyDown() then + local newScale = WorldMapFrame:GetScale() + arg1 / 10 + if newScale < 0.4 then newScale = 0.4 end + if newScale > 1.5 then newScale = 1.5 end + WorldMapFrame:SetScale(newScale) + end + end) + + HookScript(WorldMapFrame, "OnMouseDown", function() + WorldMapFrame:StartMoving() + end) + + HookScript(WorldMapFrame, "OnMouseUp", function() + WorldMapFrame:StopMovingOrSizing() + end) + + ApplyWorldMapWindowLayout() + + -- WorldMapTooltip: raw textures on a child frame (SetBackdrop is unreliable) + if WorldMapTooltip and not WorldMapTooltip._nanamiBG then + WorldMapTooltip._nanamiBG = true + + local wmtBgFrame = CreateFrame("Frame", nil, WorldMapTooltip) + wmtBgFrame:SetAllPoints(WorldMapTooltip) + wmtBgFrame:SetFrameLevel(math.max(0, WorldMapTooltip:GetFrameLevel())) + + local bg = wmtBgFrame:CreateTexture(nil, "BACKGROUND") + bg:SetTexture("Interface\\Buttons\\WHITE8X8") + bg:SetVertexColor(0.05, 0.05, 0.05, 1) + bg:SetAllPoints(wmtBgFrame) + + local function MakeEdge(p1, r1, p2, r2, w, h) + local t = wmtBgFrame:CreateTexture(nil, "BORDER") + t:SetTexture("Interface\\Buttons\\WHITE8X8") + t:SetVertexColor(0.25, 0.25, 0.25, 1) + t:SetPoint(p1, WorldMapTooltip, r1) + t:SetPoint(p2, WorldMapTooltip, r2) + if w then t:SetWidth(w) end + if h then t:SetHeight(h) end + end + MakeEdge("TOPLEFT","TOPLEFT","TOPRIGHT","TOPRIGHT", nil, 1) + MakeEdge("BOTTOMLEFT","BOTTOMLEFT","BOTTOMRIGHT","BOTTOMRIGHT", nil, 1) + MakeEdge("TOPLEFT","TOPLEFT","BOTTOMLEFT","BOTTOMLEFT", 1, nil) + MakeEdge("TOPRIGHT","TOPRIGHT","BOTTOMRIGHT","BOTTOMRIGHT", 1, nil) + end +end + +-------------------------------------------------------------------------------- +-- Cooldown Numbers +-- Display remaining duration as text on every cooldown frame (>= 2 sec). +-------------------------------------------------------------------------------- +local function TimeConvert(remaining) + local color = "|cffffffff" + if remaining < 5 then + color = "|cffff5555" + elseif remaining < 10 then + color = "|cffffff55" + end + + if remaining < 60 then + return color .. math.ceil(remaining) + elseif remaining < 3600 then + return color .. math.ceil(remaining / 60) .. "m" + elseif remaining < 86400 then + return color .. math.ceil(remaining / 3600) .. "h" + else + return color .. math.ceil(remaining / 86400) .. "d" + end +end + +local function CooldownOnUpdate() + local parent = this:GetParent() + if not parent then this:Hide() return end + + if not this.tick then this.tick = GetTime() + 0.1 end + if this.tick > GetTime() then return end + this.tick = GetTime() + 0.1 + + this:SetAlpha(parent:GetAlpha()) + + if this.start < GetTime() then + local remaining = this.duration - (GetTime() - this.start) + if remaining > 0 then + this.text:SetText(TimeConvert(remaining)) + else + this:Hide() + end + else + local time = time() + local startupTime = time - GetTime() + local cdTime = (2 ^ 32) / 1000 - this.start + local cdStartTime = startupTime - cdTime + local cdEndTime = cdStartTime + this.duration + local remaining = cdEndTime - time + if remaining >= 0 then + this.text:SetText(TimeConvert(remaining)) + else + this:Hide() + end + end +end + +local function IsActionBarButtonName(name) + if not name then return false end + return string.find(name, "^ActionButton%d+$") + or string.find(name, "^BonusActionButton%d+$") + or string.find(name, "^MultiBarBottomLeftButton%d+$") + or string.find(name, "^MultiBarBottomRightButton%d+$") + or string.find(name, "^MultiBarLeftButton%d+$") + or string.find(name, "^MultiBarRightButton%d+$") + or string.find(name, "^PetActionButton%d+$") + or string.find(name, "^ShapeshiftButton%d+$") +end + +local function UpdateActionBarCooldownMask(cooldown) + if not cooldown then return end + if cooldown.cooldownmask then + cooldown.cooldownmask:SetAllPoints(cooldown) + cooldown.cooldownmask:SetFrameStrata(cooldown:GetFrameStrata()) + cooldown.cooldownmask:SetFrameLevel(cooldown:GetFrameLevel() + 1) + end + if cooldown.cooldowntext then + cooldown.cooldowntext:SetAllPoints(cooldown) + cooldown.cooldowntext:SetFrameStrata(cooldown:GetFrameStrata()) + cooldown.cooldowntext:SetFrameLevel(cooldown:GetFrameLevel() + 2) + end +end + +local function CreateCoolDown(cooldown, start, duration) + if not cooldown then return end + local parent = cooldown:GetParent() + if not parent then return end + if cooldown.readable then return end + + local parentname = parent and parent.GetName and parent:GetName() + parentname = parentname or "UnknownCooldownFrame" + + cooldown.cooldowntext = CreateFrame("Frame", parentname .. "NanamiCDText", cooldown) + cooldown.cooldowntext:SetAllPoints(cooldown) + cooldown.cooldowntext:SetFrameStrata(cooldown:GetFrameStrata()) + cooldown.cooldowntext:SetFrameLevel(cooldown:GetFrameLevel() + 2) + cooldown.cooldowntext.text = cooldown.cooldowntext:CreateFontString( + parentname .. "NanamiCDFont", "OVERLAY") + + local isActionBar = IsActionBarButtonName(parentname) + local size = parent:GetHeight() or 0 + size = size > 0 and size * 0.64 or 12 + size = size > 14 and 14 or size + + if isActionBar then + local bigSize = size * 1.22 + if bigSize < 13 then bigSize = 13 end + if bigSize > 18 then bigSize = 18 end + cooldown.cooldowntext.text:SetFont(STANDARD_TEXT_FONT, bigSize, "THICKOUTLINE") + else + cooldown.cooldowntext.text:SetFont(STANDARD_TEXT_FONT, size, "OUTLINE") + end + cooldown.cooldowntext.text:SetDrawLayer("OVERLAY", 7) + + if isActionBar then + cooldown.cooldownmask = CreateFrame("Frame", parentname .. "NanamiCDMask", cooldown) + cooldown.cooldownmask:SetAllPoints(cooldown) + cooldown.cooldownmask:SetFrameStrata(cooldown:GetFrameStrata()) + cooldown.cooldownmask:SetFrameLevel(cooldown:GetFrameLevel() + 1) + local mask = cooldown.cooldownmask:CreateTexture(nil, "BACKGROUND") + mask:SetTexture("Interface\\Buttons\\WHITE8X8") + mask:SetAllPoints(cooldown.cooldownmask) + mask:SetDrawLayer("BACKGROUND", 0) + mask:SetVertexColor(0, 0, 0, 0.45) + cooldown.cooldownmask.mask = mask + cooldown.cooldowntext.text:SetPoint("CENTER", cooldown.cooldowntext, "CENTER", 0, 0) + else + cooldown.cooldowntext.text:SetPoint("CENTER", cooldown.cooldowntext, "CENTER", 0, 0) + end + + cooldown.cooldowntext:SetScript("OnUpdate", CooldownOnUpdate) +end + +local function SetCooldown(frame, start, duration, enable) + if not frame then return end + if frame.noCooldownCount then return end + + if not duration or duration < 2 then + if frame.cooldowntext then + frame.cooldowntext:Hide() + end + if frame.cooldownmask then + frame.cooldownmask:Hide() + end + return + end + + if not frame.cooldowntext then + CreateCoolDown(frame, start, duration) + end + + if frame.cooldowntext then + UpdateActionBarCooldownMask(frame) + if start > 0 and duration > 0 and (not enable or enable > 0) then + if frame.cooldownmask then frame.cooldownmask:Show() end + frame.cooldowntext:Show() + else + if frame.cooldownmask then frame.cooldownmask:Hide() end + frame.cooldowntext:Hide() + end + frame.cooldowntext.start = start + frame.cooldowntext.duration = duration + end +end + +local function InitCooldownNumbers() + _hooksecurefunc("CooldownFrame_SetTimer", SetCooldown) +end + +-------------------------------------------------------------------------------- +-- Quest Watch Timed Quest Countdown +-- Append remaining time for timed quests in the watched quest list (outside +-- quest detail UI), so players can see countdown directly on the main HUD. +-------------------------------------------------------------------------------- +local function FormatQuestCountdown(seconds) + local s = tonumber(seconds) or 0 + if s <= 0 then return "0s" end + + local h = math.floor(s / 3600) + local m = math.floor(math.mod(s, 3600) / 60) + local sec = math.floor(math.mod(s, 60)) + + if h > 0 then + return string.format("%dh %02dm %02ds", h, m, sec) + elseif m > 0 then + return string.format("%dm %02ds", m, sec) + end + return string.format("%ds", sec) +end + +local function InitQuestWatchCountdown() + if not QuestWatch_Update or not GetNumQuestWatches or not GetQuestLogTitle + or not GetQuestIndexForWatch or not GetQuestLogTimeLeft then + return + end + + local function StripNanamiCountdown(text) + if not text then return text end + return string.gsub(text, " |cffffcc00%[剩余: [^%]]+%]|r$", "") + end + + local applying = false + local function ApplyQuestWatchCountdown() + if applying then return end + applying = true + + local timedByTitle = {} + local watchCount = tonumber(GetNumQuestWatches()) or 0 + for i = 1, watchCount do + local questIndex = GetQuestIndexForWatch(i) + if questIndex and questIndex > 0 then + local title, level, tag, isHeader = GetQuestLogTitle(questIndex) + if title and not isHeader then + local timeLeft = GetQuestLogTimeLeft(questIndex) + if timeLeft and timeLeft > 0 then + timedByTitle[title] = FormatQuestCountdown(timeLeft) + end + end + end + end + + local lineCount = tonumber(QUESTWATCHLINES) or 0 + for i = 1, lineCount do + local fs = _G["QuestWatchLine" .. i] + if fs and fs.GetText and fs.SetText then + local text = fs:GetText() + if text and text ~= "" then + local clean = StripNanamiCountdown(text) + local timerText = timedByTitle[clean] + if timerText then + fs:SetText(clean .. " |cffffcc00[剩余: " .. timerText .. "]|r") + elseif clean ~= text then + fs:SetText(clean) + end + end + end + end + + applying = false + end + + _hooksecurefunc("QuestWatch_Update", ApplyQuestWatchCountdown) + + local ticker = CreateFrame("Frame", "NanamiQuestWatchCountdownTicker") + ticker._e = 0 + ticker:SetScript("OnUpdate", function() + this._e = (this._e or 0) + (arg1 or 0) + if this._e < 1.0 then return end + this._e = 0 + if (tonumber(GetNumQuestWatches()) or 0) > 0 then + pcall(ApplyQuestWatchCountdown) + end + end) +end + +-------------------------------------------------------------------------------- +-- Dark UI +-- Turns the entire interface into darker colors by applying vertex color +-- tinting to all UI textures recursively. +-------------------------------------------------------------------------------- +local function HookAddonOrVariable(addon, func) + local lurker = CreateFrame("Frame", nil) + lurker.func = func + lurker:RegisterEvent("ADDON_LOADED") + lurker:RegisterEvent("VARIABLES_LOADED") + lurker:RegisterEvent("PLAYER_ENTERING_WORLD") + lurker:SetScript("OnEvent", function() + if IsAddOnLoaded(addon) or getfenv(0)[addon] then + this:func() + this:UnregisterAllEvents() + end + end) +end + +local borderBackdrop = { + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 16, + insets = { left = 0, right = 0, top = 0, bottom = 0 } +} + +local function AddBorder(frame, inset, color) + if not frame then return end + if frame.NanamiBorder then return frame.NanamiBorder end + + local top, right, bottom, left + if type(inset) == "table" then + top, right, bottom, left = unpack(inset) + left, bottom = -left, -bottom + end + + frame.NanamiBorder = CreateFrame("Frame", nil, frame) + frame.NanamiBorder:SetPoint("TOPLEFT", frame, "TOPLEFT", + (left or -inset), (top or inset)) + frame.NanamiBorder:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", + (right or inset), (bottom or -inset)) + frame.NanamiBorder:SetBackdrop(borderBackdrop) + if color then + frame.NanamiBorder:SetBackdropBorderColor(color.r, color.g, color.b, 1) + end + return frame.NanamiBorder +end + +local darkColor = { r = .3, g = .3, b = .3, a = .9 } + +local darkBlacklist = { + ["Solid Texture"] = true, ["WHITE8X8"] = true, ["StatusBar"] = true, + ["BarFill"] = true, ["Portrait"] = true, ["Button"] = true, + ["Icon"] = true, ["AddOns"] = true, ["StationeryTest"] = true, + ["TargetDead"] = true, ["^KeyRing"] = true, ["GossipIcon"] = true, + ["WorldMap\\(.+)\\"] = true, ["PetHappiness"] = true, + ["Elite"] = true, ["Rare"] = true, ["ColorPickerWheel"] = true, + ["ComboPoint"] = true, ["Skull"] = true, + ["battlenetworking0"] = true, ["damage"] = true, + ["tank"] = true, ["healer"] = true, +} + +local darkRegionSkips = { + ["ColorPickerFrame"] = { [15] = true } +} + +local darkBackgrounds = { + ["^SpellBookFrame$"] = { 325, 355, 17, -74 }, + ["^ItemTextFrame$"] = { 300, 355, 24, -74 }, +} + +local darkBorders = { + ["ShapeshiftButton"] = 3, ["BuffButton"] = 3, + ["TargetFrameBuff"] = 3, ["TempEnchant"] = 3, + ["SpellButton"] = 3, ["SpellBookSkillLineTab"] = 3, + ["ActionButton%d+$"] = 3, ["MultiBar(.+)Button%d+$"] = 3, + ["KeyRingButton"] = 2, + ["ActionBarUpButton"] = -3, ["ActionBarDownButton"] = -3, + ["Character(.+)Slot$"] = 3, ["Inspect(.+)Slot$"] = 3, + ["ContainerFrame(.+)Item"] = 3, ["MainMenuBarBackpackButton$"] = 3, + ["CharacterBag(.+)Slot$"] = 3, ["ChatFrame(.+)Button"] = -2, + ["PetFrameHappiness"] = 2, ["MicroButton"] = { -21, 0, 0, 0 }, +} + +local darkAddonFrames = { + ["Blizzard_TalentUI"] = { "TalentFrame" }, + ["Blizzard_AuctionUI"] = { "AuctionFrame", "AuctionDressUpFrame" }, + ["Blizzard_CraftUI"] = { "CraftFrame" }, + ["Blizzard_InspectUI"] = { "InspectPaperDollFrame", "InspectHonorFrame", "InspectFrameTab1", "InspectFrameTab2" }, + ["Blizzard_MacroUI"] = { "MacroFrame", "MacroPopupFrame" }, + ["Blizzard_RaidUI"] = { "ReadyCheckFrame" }, + ["Blizzard_TradeSkillUI"] = { "TradeSkillFrame" }, + -- ClassTrainerFrame replaced by TrainerUI.lua +} + +local function IsDarkBlacklisted(texture) + local name = texture:GetName() + local tex = texture:GetTexture() + if not tex then return true end + + if name then + for entry in pairs(darkBlacklist) do + if string.find(name, entry, 1) then return true end + end + end + + for entry in pairs(darkBlacklist) do + if string.find(tex, entry, 1) then return true end + end + return nil +end + +local function AddSpecialBackground(frame, w, h, x, y) + frame.NanamiMaterial = frame.NanamiMaterial or frame:CreateTexture(nil, "OVERLAY") + frame.NanamiMaterial:SetTexture("Interface\\Stationery\\StationeryTest1") + frame.NanamiMaterial:SetWidth(w) + frame.NanamiMaterial:SetHeight(h) + frame.NanamiMaterial:SetPoint("TOPLEFT", frame, x, y) + frame.NanamiMaterial:SetVertexColor(.8, .8, .8) +end + +local darkFrameSkips = { + ["^SFramesChat"] = true, + ["^SFramesPlayer"] = true, + ["^SFramesTarget"] = true, + ["^SFramesParty"] = true, + ["^SFramesRaid"] = true, + ["^SFramesMBuff"] = true, + ["^SFramesMDebuff"] = true, + ["^GameTooltip"] = true, +} + +local function DarkenFrame(frame, r, g, b, a) + if not r and not g and not b then + r, g, b, a = darkColor.r, darkColor.g, darkColor.b, darkColor.a + end + + local fname = frame and frame.GetName and frame:GetName() + if fname then + for pattern in pairs(darkFrameSkips) do + if string.find(fname, pattern) then return end + end + end + + if frame and frame.GetChildren then + for _, child in pairs({ frame:GetChildren() }) do + DarkenFrame(child, r, g, b, a) + end + end + + if frame and frame.GetRegions then + local name = frame.GetName and frame:GetName() + + if frame.SetBackdropBorderColor then + frame:SetBackdropBorderColor(darkColor.r, darkColor.g, darkColor.b, darkColor.a) + end + + for pattern, inset in pairs(darkBackgrounds) do + if name and string.find(name, pattern) then + AddSpecialBackground(frame, inset[1], inset[2], inset[3], inset[4]) + end + end + + for pattern, inset in pairs(darkBorders) do + if name and string.find(name, pattern) then + AddBorder(frame, inset, darkColor) + end + end + + for id, region in pairs({ frame:GetRegions() }) do + if region.SetVertexColor and region:GetObjectType() == "Texture" then + if name and id and darkRegionSkips[name] and darkRegionSkips[name][id] then + -- skip + elseif region.GetBlendMode and region:GetBlendMode() == "ADD" then + -- skip blend textures + elseif IsDarkBlacklisted(region) then + -- skip blacklisted + else + region:SetVertexColor(r, g, b, a) + end + end + end + end +end + +-------------------------------------------------------------------------------- +-- StaticPopup Theme Skin +-------------------------------------------------------------------------------- +local function InitPopupSkin() + local popupSkinned = {} + local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARKai_T.ttf" + + local _A = SFrames.ActiveTheme + local P = { + bg = _A.panelBg or { 0.12, 0.06, 0.10, 0.95 }, + border = _A.panelBorder or { 0.55, 0.30, 0.42, 0.9 }, + btnBg = _A.btnBg or { 0.18, 0.10, 0.15, 0.94 }, + btnBd = _A.btnBorder or { 0.50, 0.30, 0.40, 0.80 }, + text = _A.nameText or { 0.90, 0.88, 0.94 }, + } + + local function SkinButton(btn) + if not btn then return end + local regions = { btn:GetRegions() } + for _, r in ipairs(regions) do + if r and r.SetTexture and r:GetObjectType() == "Texture" then + local tex = r:GetTexture() + if tex and type(tex) == "string" and (string.find(tex, "UI%-Panel") or string.find(tex, "UI%-DialogBox")) then + r:Hide() + end + end + end + + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + btn:SetBackdropColor(P.btnBg[1], P.btnBg[2], P.btnBg[3], P.btnBg[4]) + btn:SetBackdropBorderColor(P.btnBd[1], P.btnBd[2], P.btnBd[3], P.btnBd[4]) + + local fs = btn:GetFontString() + if fs then + fs:SetFont(font, 12, "OUTLINE") + fs:SetTextColor(P.text[1], P.text[2], P.text[3]) + end + end + + local function SkinPopupFrame(frame) + if not frame then return end + local frameName = frame:GetName() + if not frameName then return end + + if not popupSkinned[frameName] then + popupSkinned[frameName] = true + + local regions = { frame:GetRegions() } + for _, r in ipairs(regions) do + if r and r:GetObjectType() == "Texture" then + local dl = r:GetDrawLayer() + if dl == "BACKGROUND" or dl == "BORDER" or dl == "ARTWORK" then + local tex = r:GetTexture() or "" + if type(tex) == "string" and (string.find(tex, "UI%-DialogBox") or string.find(tex, "UI%-Panel")) then + r:Hide() + end + end + end + end + + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + frame:SetBackdropColor(P.bg[1], P.bg[2], P.bg[3], P.bg[4]) + frame:SetBackdropBorderColor(P.border[1], P.border[2], P.border[3], P.border[4]) + + local textFS = _G[frameName .. "Text"] + if textFS and textFS.SetFont then + textFS:SetFont(font, 13, "OUTLINE") + textFS:SetTextColor(P.text[1], P.text[2], P.text[3]) + end + + local editBox = _G[frameName .. "EditBox"] + if editBox then + editBox:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + editBox:SetBackdropColor(0.05, 0.03, 0.05, 0.9) + editBox:SetBackdropBorderColor(P.btnBd[1], P.btnBd[2], P.btnBd[3], P.btnBd[4]) + editBox:SetFont(font, 12, "OUTLINE") + editBox:SetTextColor(P.text[1], P.text[2], P.text[3]) + end + + local moneyFrame = _G[frameName .. "MoneyFrame"] + if moneyFrame then + local mRegions = { moneyFrame:GetRegions() } + for _, r in ipairs(mRegions) do + if r and r.SetFont then r:SetFont(font, 12, "OUTLINE") end + end + end + end + + for _, suffix in ipairs({"Button1", "Button2", "Button3"}) do + local btn = _G[frameName .. suffix] + if btn then SkinButton(btn) end + end + end + + for i = 1, 4 do + local f = _G["StaticPopup" .. i] + if f then pcall(SkinPopupFrame, f) end + end + + if StaticPopup_Show then + local origShow = StaticPopup_Show + StaticPopup_Show = function(a1, a2, a3, a4) + local dialog = origShow(a1, a2, a3, a4) + if dialog then + pcall(SkinPopupFrame, dialog) + end + return dialog + end + end +end + +local function InitDarkUI() + local hookBuffButton_Update = BuffButton_Update + BuffButton_Update = function(buttonName, index, filter) + hookBuffButton_Update(buttonName, index, filter) + local name = buttonName and index and buttonName .. index or this:GetName() + local original = getfenv(0)[name .. "Border"] + if original and this.NanamiBorder then + local r, g, b = original:GetVertexColor() + this.NanamiBorder:SetBackdropBorderColor(r, g, b, 1) + original:SetAlpha(0) + end + end + + TOOLTIP_DEFAULT_COLOR.r = darkColor.r + TOOLTIP_DEFAULT_COLOR.g = darkColor.g + TOOLTIP_DEFAULT_COLOR.b = darkColor.b + + TOOLTIP_DEFAULT_BACKGROUND_COLOR.r = darkColor.r + TOOLTIP_DEFAULT_BACKGROUND_COLOR.g = darkColor.g + TOOLTIP_DEFAULT_BACKGROUND_COLOR.b = darkColor.b + + DarkenFrame(UIParent) + DarkenFrame(WorldMapFrame) + DarkenFrame(DropDownList1) + DarkenFrame(DropDownList2) + DarkenFrame(DropDownList3) + + local bars = { "Action", "BonusAction", "MultiBarBottomLeft", + "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight", "Shapeshift" } + for _, prefix in pairs(bars) do + for i = 1, NUM_ACTIONBAR_BUTTONS do + local button = getfenv(0)[prefix .. "Button" .. i] + local texture = getfenv(0)[prefix .. "Button" .. i .. "NormalTexture"] + if button and texture then + texture:SetWidth(60) + texture:SetHeight(60) + texture:SetPoint("CENTER", 0, 0) + AddBorder(button, 3) + end + end + end + + for _, button in pairs({ MinimapZoomOut, MinimapZoomIn }) do + for _, func in pairs({ "GetNormalTexture", "GetDisabledTexture", "GetPushedTexture" }) do + if button[func] then + local tex = button[func](button) + if tex then + tex:SetVertexColor(darkColor.r + .2, darkColor.g + .2, darkColor.b + .2, 1) + end + end + end + end + + for addon, data in pairs(darkAddonFrames) do + local skip = false + if SFramesDB and SFramesDB.enableTradeSkill ~= false then + if addon == "Blizzard_TradeSkillUI" or addon == "Blizzard_CraftUI" then + skip = true + end + end + if not skip then + for _, frameName in pairs(data) do + local fn = frameName + HookAddonOrVariable(fn, function() + DarkenFrame(getfenv(0)[fn]) + end) + end + end + end + + HookAddonOrVariable("Blizzard_TimeManager", function() + DarkenFrame(TimeManagerClockButton) + end) + + HookAddonOrVariable("GameTooltipStatusBarBackdrop", function() + DarkenFrame(getfenv(0)["GameTooltipStatusBarBackdrop"]) + end) +end + +-------------------------------------------------------------------------------- +-- Module API +-------------------------------------------------------------------------------- +function Tweaks:Initialize() + local cfg = GetTweaksCfg() + + if cfg.autoStance ~= false then + local ok, err = pcall(InitAutoStance) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: AutoStance init failed: " .. tostring(err) .. "|r") + end + end + + if cfg.autoDismount ~= false then + local ok, err = pcall(InitAutoDismount) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: AutoDismount init failed: " .. tostring(err) .. "|r") + end + end + + if cfg.superWoW ~= false then + local ok, err = pcall(InitSuperWoW) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: SuperWoW init failed: " .. tostring(err) .. "|r") + end + end + + if cfg.turtleCompat ~= false then + local ok, err = pcall(InitTurtleCompat) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: TurtleCompat init failed: " .. tostring(err) .. "|r") + end + end + + if SFrames.WorldMap and SFrames.WorldMap.initialized then + -- New WorldMap module has taken over; skip legacy code + elseif cfg.worldMapWindow ~= false then + local ok, err = pcall(InitWorldMapWindow) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: WorldMapWindow init failed: " .. tostring(err) .. "|r") + end + end + + if cfg.cooldownNumbers ~= false then + local ok, err = pcall(InitCooldownNumbers) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: CooldownNumbers init failed: " .. tostring(err) .. "|r") + end + end + + do + local ok, err = pcall(InitQuestWatchCountdown) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: QuestWatchCountdown init failed: " .. tostring(err) .. "|r") + end + end + + if cfg.darkUI then + local ok, err = pcall(InitDarkUI) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: DarkUI init failed: " .. tostring(err) .. "|r") + end + end + + do + local ok, err = pcall(InitPopupSkin) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: PopupSkin init failed: " .. tostring(err) .. "|r") + end + end +end diff --git a/Units/Party.lua b/Units/Party.lua new file mode 100644 index 0000000..cf2a639 --- /dev/null +++ b/Units/Party.lua @@ -0,0 +1,1176 @@ +SFrames.Party = {} +local _A = SFrames.ActiveTheme + +local PARTY_FRAME_WIDTH = 150 +local PARTY_FRAME_HEIGHT = 35 +local PARTY_VERTICAL_GAP = 30 +local PARTY_HORIZONTAL_GAP = 8 + +local PARTY_UNIT_LOOKUP = { party1 = true, party2 = true, party3 = true, party4 = true } +local PARTYPET_UNIT_LOOKUP = { partypet1 = true, partypet2 = true, partypet3 = true, partypet4 = true } + +local function GetIncomingHeals(unit) + return SFrames:GetIncomingHeals(unit) +end + +local function TryDropCursorOnUnit(unit) + if not unit or not UnitExists(unit) then return false end + if not CursorHasItem or not CursorHasItem() then return false end + if not DropItemOnUnit then return false end + + local ok = pcall(DropItemOnUnit, unit) + if not ok then + return false + end + + return not CursorHasItem() +end + +local function Clamp(value, minValue, maxValue) + if value < minValue then + return minValue + end + if value > maxValue then + return maxValue + end + return value +end + +function SFrames.Party:GetMetrics() + local db = SFramesDB or {} + + local width = tonumber(db.partyFrameWidth) or PARTY_FRAME_WIDTH + width = Clamp(math.floor(width + 0.5), 120, 320) + + local height = tonumber(db.partyFrameHeight) or PARTY_FRAME_HEIGHT + height = Clamp(math.floor(height + 0.5), 28, 80) + + local portraitWidth = tonumber(db.partyPortraitWidth) or math.min(33, height - 2) + portraitWidth = Clamp(math.floor(portraitWidth + 0.5), 24, 64) + if portraitWidth > width - 70 then + portraitWidth = width - 70 + end + + local healthHeight = tonumber(db.partyHealthHeight) or math.floor((height - 3) * 0.7) + healthHeight = Clamp(math.floor(healthHeight + 0.5), 10, height - 8) + + local powerHeight = tonumber(db.partyPowerHeight) or (height - healthHeight - 3) + powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, height - 6) + + if healthHeight + powerHeight + 3 > height then + powerHeight = height - healthHeight - 3 + if powerHeight < 6 then + powerHeight = 6 + healthHeight = height - powerHeight - 3 + if healthHeight < 10 then + healthHeight = 10 + powerHeight = height - healthHeight - 3 + end + end + end + + local hgap = tonumber(db.partyHorizontalGap) or PARTY_HORIZONTAL_GAP + hgap = Clamp(math.floor(hgap + 0.5), 0, 40) + + local vgap = tonumber(db.partyVerticalGap) or PARTY_VERTICAL_GAP + vgap = Clamp(math.floor(vgap + 0.5), 0, 80) + + local nameFont = tonumber(db.partyNameFontSize) or 10 + nameFont = Clamp(math.floor(nameFont + 0.5), 8, 18) + + local valueFont = tonumber(db.partyValueFontSize) or 10 + valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18) + + return { + width = width, + height = height, + portraitWidth = portraitWidth, + healthHeight = healthHeight, + powerHeight = powerHeight, + horizontalGap = hgap, + verticalGap = vgap, + nameFont = nameFont, + valueFont = valueFont, + } +end + +function SFrames.Party:ApplyFrameStyle(frame, metrics) + if not frame then return end + + frame:SetWidth(metrics.width) + frame:SetHeight(metrics.height) + + if frame.pbg then + frame.pbg:SetWidth(metrics.portraitWidth + 2) + frame.pbg:SetHeight(metrics.height) + frame.pbg:ClearAllPoints() + frame.pbg:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0) + end + + if frame.portrait then + frame.portrait:SetWidth(metrics.portraitWidth) + frame.portrait:SetHeight(metrics.height - 2) + if frame.pbg then + frame.portrait:ClearAllPoints() + frame.portrait:SetPoint("CENTER", frame.pbg, "CENTER", 0, 0) + end + end + + if frame.health then + frame.health:ClearAllPoints() + frame.health:SetPoint("TOPLEFT", frame.pbg, "TOPRIGHT", 2, -1) + frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1) + frame.health:SetHeight(metrics.healthHeight) + end + + if frame.healthBGFrame then + frame.healthBGFrame:ClearAllPoints() + frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", -1, 1) + frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 1, -1) + end + + if frame.power then + frame.power:ClearAllPoints() + frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1) + frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0) + frame.power:SetHeight(metrics.powerHeight) + end + + if frame.powerBGFrame then + frame.powerBGFrame:ClearAllPoints() + frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", -1, 1) + frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 1, -1) + end + + local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" + local fontPath = SFrames:GetFont() + + if frame.nameText then + frame.nameText:SetFont(fontPath, metrics.nameFont, outline) + end + if frame.healthText then + frame.healthText:SetFont(fontPath, metrics.valueFont, outline) + end +end + +function SFrames.Party:ApplyConfig() + if not self.parent then return end + + local frameScale = tonumber(SFramesDB and SFramesDB.partyFrameScale) or 1 + frameScale = Clamp(frameScale, 0.7, 1.8) + self.parent:SetScale(frameScale) + + local metrics = self:GetMetrics() + self.metrics = metrics + + if self.frames then + for i = 1, 4 do + local item = self.frames[i] + if item and item.frame then + self:ApplyFrameStyle(item.frame, metrics) + end + end + end + + self:ApplyLayout() + if self.testing then + for i = 1, 4 do + if self.frames[i] and self.frames[i].frame then + self.frames[i].frame:Show() + end + end + else + self:UpdateAll() + end +end + +function SFrames.Party:GetLayoutMode() + if SFramesDB and SFramesDB.partyLayout == "horizontal" then + return "horizontal" + end + return "vertical" +end + +function SFrames.Party:SavePosition() + if not (self.parent and SFramesDB) then return end + if not SFramesDB.Positions then SFramesDB.Positions = {} end + local point, _, relativePoint, xOfs, yOfs = self.parent:GetPoint() + if point and relativePoint then + SFramesDB.Positions["PartyFrame"] = { + point = point, + relativePoint = relativePoint, + xOfs = xOfs or 0, + yOfs = yOfs or 0, + } + end +end + +function SFrames.Party:ApplyPosition() + if not self.parent then return end + self.parent:ClearAllPoints() + local pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PartyFrame"] + if pos and pos.point and pos.relativePoint and type(pos.xOfs) == "number" and type(pos.yOfs) == "number" then + self.parent:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs) + else + self.parent:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 15, -150) + end +end + +function SFrames.Party:ApplyLayout() + if not (self.parent and self.frames) then return end + + local metrics = self.metrics or self:GetMetrics() + self.metrics = metrics + local mode = self:GetLayoutMode() + for i = 1, 4 do + local f = self.frames[i] and self.frames[i].frame + if f then + f:ClearAllPoints() + if i == 1 then + f:SetPoint("TOPLEFT", self.parent, "TOPLEFT", 0, 0) + else + local prev = self.frames[i - 1].frame + if mode == "horizontal" then + f:SetPoint("TOPLEFT", prev, "TOPRIGHT", metrics.horizontalGap, 0) + else + f:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -metrics.verticalGap) + end + end + end + end + + if mode == "horizontal" then + self.parent:SetWidth((metrics.width * 4) + (metrics.horizontalGap * 3)) + self.parent:SetHeight(metrics.height) + else + self.parent:SetWidth(metrics.width) + self.parent:SetHeight(metrics.height + ((metrics.height + metrics.verticalGap) * 3)) + end +end + +function SFrames.Party:SetLayout(mode) + if not SFramesDB then SFramesDB = {} end + if mode ~= "horizontal" then + mode = "vertical" + end + SFramesDB.partyLayout = mode + self:ApplyLayout() + if self.testing then + for i = 1, 4 do + self.frames[i].frame:Show() + end + else + self:UpdateAll() + end +end + +function SFrames.Party:Initialize() + self.frames = {} + + if not SFramesDB then SFramesDB = {} end + if not SFramesDB.partyLayout then + SFramesDB.partyLayout = "vertical" + end + + local parent = CreateFrame("Frame", "SFramesPartyParent", UIParent) + parent:SetWidth(PARTY_FRAME_WIDTH) + parent:SetHeight(PARTY_FRAME_HEIGHT) + local frameScale = (SFramesDB and type(SFramesDB.partyFrameScale) == "number") and SFramesDB.partyFrameScale or 1 + parent:SetScale(frameScale) + self.parent = parent + self:ApplyPosition() + + parent:SetMovable(true) + + for i = 1, 4 do + local unit = "party" .. i + local f = CreateFrame("Button", "SFramesPartyFrame"..i, parent) + f:SetWidth(PARTY_FRAME_WIDTH) + f:SetHeight(PARTY_FRAME_HEIGHT) + + f.id = i + f:RegisterForClicks("LeftButtonUp", "RightButtonUp") + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() + if IsAltKeyDown() or SFrames.isUnlocked then + SFrames.Party.parent:StartMoving() + end + end) + f:SetScript("OnDragStop", function() + SFrames.Party.parent:StopMovingOrSizing() + SFrames.Party:SavePosition() + end) + f:SetScript("OnClick", function() + if arg1 == "LeftButton" then + if TryDropCursorOnUnit(this.unit) then + return + end + if SpellIsTargeting and SpellIsTargeting() then + SpellTargetUnit(this.unit) + return + end + TargetUnit(this.unit) + SFrames.Party:UpdateFrame(this.unit) + elseif arg1 == "RightButton" then + ToggleDropDownMenu(1, nil, getglobal("PartyMemberFrame"..this.id.."DropDown"), "cursor") + end + end) + f:SetScript("OnReceiveDrag", function() + if TryDropCursorOnUnit(this.unit) then + return + end + if SpellIsTargeting and SpellIsTargeting() then + SpellTargetUnit(this.unit) + end + end) + f:SetScript("OnEnter", function() + GameTooltip_SetDefaultAnchor(GameTooltip, this) + GameTooltip:SetUnit(this.unit) + end) + f:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + f.unit = unit + + -- Portrait + local pbg = CreateFrame("Frame", nil, f) + pbg:SetWidth(35) + pbg:SetHeight(35) + pbg:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) + pbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(pbg) + f.pbg = pbg + + f.portrait = CreateFrame("PlayerModel", nil, f) + f.portrait:SetWidth(33) + f.portrait:SetHeight(33) + f.portrait:SetPoint("CENTER", pbg, "CENTER", 0, 0) + + local offlineOverlay = CreateFrame("Frame", nil, f) + offlineOverlay:SetFrameLevel((f.portrait:GetFrameLevel() or 0) + 3) + offlineOverlay:SetAllPoints(pbg) + local offlineIcon = SFrames:CreateIcon(offlineOverlay, "offline", 20) + offlineIcon:SetPoint("CENTER", offlineOverlay, "CENTER", 0, 0) + offlineIcon:SetVertexColor(0.7, 0.7, 0.7, 0.9) + offlineIcon:Hide() + f.offlineIcon = offlineIcon + + -- Health Bar + f.health = SFrames:CreateStatusBar(f, "SFramesPartyFrame"..i.."Health") + f.health:SetPoint("TOPLEFT", pbg, "TOPRIGHT", 2, -1) + f.health:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 10) + + local hbg = CreateFrame("Frame", nil, f) + hbg:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1) + hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) + hbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(hbg) + f.healthBGFrame = hbg + + f.health.bg = f.health:CreateTexture(nil, "BACKGROUND") + f.health.bg:SetAllPoints() + f.health.bg:SetTexture(SFrames:GetTexture()) + f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + -- Heal prediction overlay (incoming heals) + f.health.healPredMine = f.health:CreateTexture(nil, "OVERLAY") + f.health.healPredMine:SetTexture(SFrames:GetTexture()) + f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78) + f.health.healPredMine:Hide() + + f.health.healPredOther = f.health:CreateTexture(nil, "OVERLAY") + f.health.healPredOther:SetTexture(SFrames:GetTexture()) + f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5) + f.health.healPredOther:Hide() + + -- Power Bar + f.power = SFrames:CreateStatusBar(f, "SFramesPartyFrame"..i.."Power") + f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) + f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) + f.power:SetMinMaxValues(0, 100) + + local powerbg = CreateFrame("Frame", nil, f) + powerbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1) + powerbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) + powerbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(powerbg) + f.powerBGFrame = powerbg + + f.power.bg = f.power:CreateTexture(nil, "BACKGROUND") + f.power.bg:SetAllPoints() + f.power.bg:SetTexture(SFrames:GetTexture()) + f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + -- Texts + f.nameText = SFrames:CreateFontString(f.health, 10, "LEFT") + f.nameText:SetPoint("LEFT", f.health, "LEFT", 4, 0) + + f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT") + f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0) + + f.nameText:SetShadowColor(0, 0, 0, 1) + f.nameText:SetShadowOffset(1, -1) + f.healthText:SetShadowColor(0, 0, 0, 1) + f.healthText:SetShadowOffset(1, -1) + + -- Leader / Master Looter overlay (high frame level so icons aren't hidden by portrait) + local roleOvr = CreateFrame("Frame", nil, f) + roleOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 4) + roleOvr:SetAllPoints(f) + + -- Leader Icon + f.leaderIcon = roleOvr:CreateTexture(nil, "OVERLAY") + f.leaderIcon:SetWidth(14) + f.leaderIcon:SetHeight(14) + f.leaderIcon:SetPoint("TOPLEFT", pbg, "TOPLEFT", -4, 4) + f.leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-LeaderIcon") + f.leaderIcon:Hide() + + -- Master Looter Icon + f.masterIcon = roleOvr:CreateTexture(nil, "OVERLAY") + f.masterIcon:SetWidth(12) + f.masterIcon:SetHeight(12) + f.masterIcon:SetPoint("TOPRIGHT", pbg, "TOPRIGHT", 4, 4) + f.masterIcon:SetTexture("Interface\\GroupFrame\\UI-Group-MasterLooter") + f.masterIcon:Hide() + + -- Raid Target Icon + local raidIconSize = 18 + local raidIconOvr = CreateFrame("Frame", nil, f) + raidIconOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 5) + raidIconOvr:SetWidth(raidIconSize) + raidIconOvr:SetHeight(raidIconSize) + raidIconOvr:SetPoint("CENTER", f.health, "TOP", 0, 0) + f.raidIcon = raidIconOvr:CreateTexture(nil, "OVERLAY") + f.raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons") + f.raidIcon:SetAllPoints(raidIconOvr) + f.raidIcon:Hide() + f.raidIconOverlay = raidIconOvr + + -- Pet Frame + local pf = CreateFrame("Button", "SFramesPartyPetFrame"..i, f) + pf:SetHeight(8) + pf:SetPoint("BOTTOMLEFT", f.health, "TOPLEFT", 0, 2) + pf:SetPoint("BOTTOMRIGHT", f.health, "TOPRIGHT", 0, 2) + pf.unit = "partypet"..i + + SFrames:CreateUnitBackdrop(pf) + pf.health = SFrames:CreateStatusBar(pf, "SFramesPartyPetHealth"..i) + pf.health:SetAllPoints() + pf.health.bg = pf.health:CreateTexture(nil, "BACKGROUND") + pf.health.bg:SetAllPoints() + pf.health.bg:SetTexture(SFrames:GetTexture()) + pf.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + pf.nameText = SFrames:CreateFontString(pf.health, 8, "LEFT") + pf.nameText:SetPoint("LEFT", pf.health, "LEFT", 2, 0) + pf.nameText:SetShadowColor(0, 0, 0, 1) + pf.nameText:SetShadowOffset(1, -1) + + pf:RegisterForClicks("LeftButtonUp", "RightButtonUp") + pf:SetScript("OnClick", function() TargetUnit(this.unit) end) + pf:SetScript("OnEnter", function() + GameTooltip_SetDefaultAnchor(GameTooltip, this) + GameTooltip:SetUnit(this.unit) + end) + pf:SetScript("OnLeave", function() GameTooltip:Hide() end) + pf:Hide() + f.petFrame = pf + + self.frames[i] = { frame = f, unit = unit, index = i } + f.unit = unit + + self:CreateAuras(i) + + f:SetScript("OnShow", function() + if not SFrames.Party.testing then + SFrames.Party:UpdateFrame(this.unit) + end + end) + + f:Hide() + end + + if not self._globalUpdateFrame then + self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent) + self._globalUpdateFrame:SetScript("OnUpdate", function() + if SFrames.Party.testing then return end + local dt = arg1 + local frames = SFrames.Party.frames + if not frames then return end + for i = 1, 4 do + local entry = frames[i] + if entry then + local f = entry.frame + if f:IsVisible() and f.unit and f.unit ~= "" then + f.rangeTimer = f.rangeTimer + dt + if f.rangeTimer >= 0.5 then + if UnitExists(f.unit) and not CheckInteractDistance(f.unit, 4) then + f:SetAlpha(0.5) + else + f:SetAlpha(1.0) + end + f.rangeTimer = 0 + end + + f.auraScanTimer = f.auraScanTimer + dt + if f.auraScanTimer >= 0.5 then + SFrames.Party:UpdateAuras(f.unit) + f.auraScanTimer = 0 + end + + f.healPredTimer = f.healPredTimer + dt + if f.healPredTimer >= 0.2 then + SFrames.Party:UpdateHealPrediction(f.unit) + f.healPredTimer = 0 + end + + f.tickAuraTimer = f.tickAuraTimer + dt + if f.tickAuraTimer >= 0.5 then + SFrames.Party:TickAuras(f.unit) + f.tickAuraTimer = 0 + end + end + end + end + end) + end + + self:ApplyConfig() + + SFrames:RegisterEvent("PARTY_MEMBERS_CHANGED", function() self:UpdateAll() end) + SFrames:RegisterEvent("RAID_ROSTER_UPDATE", function() self:UpdateAll() end) + SFrames:RegisterEvent("UNIT_HEALTH", function() + if PARTY_UNIT_LOOKUP[arg1] then self:UpdateHealth(arg1) + elseif PARTYPET_UNIT_LOOKUP[arg1] then self:UpdatePet(arg1) end + end) + SFrames:RegisterEvent("UNIT_MAXHEALTH", function() + if PARTY_UNIT_LOOKUP[arg1] then self:UpdateHealth(arg1) + elseif PARTYPET_UNIT_LOOKUP[arg1] then self:UpdatePet(arg1) end + end) + SFrames:RegisterEvent("UNIT_MANA", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end) + SFrames:RegisterEvent("UNIT_MAXMANA", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end) + SFrames:RegisterEvent("UNIT_RAGE", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end) + SFrames:RegisterEvent("UNIT_MAXRAGE", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end) + SFrames:RegisterEvent("UNIT_ENERGY", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end) + SFrames:RegisterEvent("UNIT_MAXENERGY", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end) + SFrames:RegisterEvent("UNIT_FOCUS", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end) + SFrames:RegisterEvent("UNIT_MAXFOCUS", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end) + SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePowerType(arg1) end end) + SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePortrait(arg1) end end) + SFrames:RegisterEvent("UNIT_AURA", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdateAuras(arg1) end end) + SFrames:RegisterEvent("UNIT_LEVEL", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdateFrame(arg1) end end) + SFrames:RegisterEvent("PARTY_LEADER_CHANGED", function() self:UpdateAll() end) + SFrames:RegisterEvent("PARTY_LOOT_METHOD_CHANGED", function() self:UpdateAll() end) + SFrames:RegisterEvent("UNIT_PET", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePet("partypet" .. string.sub(arg1, 6)) end end) + SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcons() end) + + -- Bulletproof Party load syncer (watches for count changes for 10 seconds after reload) + local syncWatcher = CreateFrame("Frame") + local syncTimer = 0 + local lastCount = -1 + syncWatcher:SetScript("OnUpdate", function() + syncTimer = syncTimer + arg1 + local count = GetNumPartyMembers() + if count ~= lastCount then + lastCount = count + SFrames.Party:UpdateAll() + end + if syncTimer > 10.0 then + this:SetScript("OnUpdate", nil) + end + end) + + self:UpdateAll() +end + +function SFrames.Party:CreateAuras(index) + local f = self.frames[index].frame + f.buffs = {} + f.debuffs = {} + local size = 20 + local spacing = 2 + + -- Party Buffs + for i = 1, 4 do + local b = CreateFrame("Button", "SFramesParty"..index.."Buff"..i, f) + b:SetWidth(size) + b:SetHeight(size) + SFrames:CreateUnitBackdrop(b) + + b.icon = b:CreateTexture(nil, "ARTWORK") + b.icon:SetAllPoints() + b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + + b.cdText = SFrames:CreateFontString(b, 9, "CENTER") + b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1) + b.cdText:SetTextColor(1, 0.82, 0) + b.cdText:SetShadowColor(0, 0, 0, 1) + b.cdText:SetShadowOffset(1, -1) + + b:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") + GameTooltip:SetUnitBuff(f.unit, this:GetID()) + end) + b:SetScript("OnLeave", function() GameTooltip:Hide() end) + + -- Anchored BELOW the frame on the left side + if i == 1 then + b:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -2) + else + b:SetPoint("LEFT", f.buffs[i-1], "RIGHT", spacing, 0) + end + + b:Hide() + f.buffs[i] = b + end + + -- Debuffs (Starting right after Buffs to remain linear) + for i = 1, 4 do + local b = CreateFrame("Button", "SFramesParty"..index.."Debuff"..i, f) + b:SetWidth(size) + b:SetHeight(size) + SFrames:CreateUnitBackdrop(b) + + b.icon = b:CreateTexture(nil, "ARTWORK") + b.icon:SetAllPoints() + b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + + b.cdText = SFrames:CreateFontString(b, 9, "CENTER") + b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1) + b.cdText:SetTextColor(1, 0.82, 0) + b.cdText:SetShadowColor(0, 0, 0, 1) + b.cdText:SetShadowOffset(1, -1) + + b:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") + GameTooltip:SetUnitDebuff(f.unit, this:GetID()) + end) + b:SetScript("OnLeave", function() GameTooltip:Hide() end) + + if i == 1 then + b:SetPoint("LEFT", f.buffs[4], "RIGHT", spacing * 4, 0) + else + b:SetPoint("LEFT", f.debuffs[i-1], "RIGHT", spacing, 0) + end + + b:Hide() + f.debuffs[i] = b + end + + f.auraScanTimer = 0 + f.rangeTimer = 0 + f.healPredTimer = 0 + f.tickAuraTimer = 0 +end + +function SFrames.Party:UpdatePet(unit) + local _, _, indexStr = string.find(unit, "(%d+)") + local index = tonumber(indexStr) + if not index or not self.frames[index] then return end + local pf = self.frames[index].frame.petFrame + if not pf then return end + + if UnitExists(unit) and UnitIsConnected("party"..index) then + pf:Show() + local name = UnitName(unit) + if name == UNKNOWNOBJECT or name == "未知目标" or name == "Unknown" then + name = "宠物" + end + pf.nameText:SetText(name) + + local hp = UnitHealth(unit) + local maxHp = UnitHealthMax(unit) + pf.health:SetMinMaxValues(0, maxHp) + pf.health:SetValue(hp) + pf.health:SetStatusBarColor(0.2, 0.8, 0.2) + else + pf:Hide() + end +end + + +function SFrames.Party:TickAuras(unit) + local data = self:GetFrameByUnit(unit) + if not data then return end + local f = data.frame + local timeNow = GetTime() + + for i = 1, 4 do + local b = f.buffs[i] + if b:IsShown() and b.expirationTime then + local timeLeft = b.expirationTime - timeNow + if timeLeft > 0 and timeLeft < 3600 then + b.cdText:SetText(SFrames:FormatTime(timeLeft)) + else + b.cdText:SetText("") + end + end + + local db = f.debuffs[i] + if db:IsShown() and db.expirationTime then + local timeLeft = db.expirationTime - timeNow + if timeLeft > 0 and timeLeft < 3600 then + db.cdText:SetText(SFrames:FormatTime(timeLeft)) + else + db.cdText:SetText("") + end + end + end +end + +function SFrames.Party:GetFrameByUnit(unit) + for i = 1, 4 do + if self.frames[i].unit == unit then + return self.frames[i] + end + end + return nil +end + +function SFrames.Party:UpdateAll() + if self.testing then return end + + local inRaid = GetNumRaidMembers() > 0 + local raidFramesEnabled = SFramesDB and SFramesDB.enableRaidFrames ~= false + + if inRaid and raidFramesEnabled then + for i = 1, 4 do + if self.frames[i] and self.frames[i].frame then + self.frames[i].frame:Hide() + end + end + return + end + if self.testing then return end + local numParty = GetNumPartyMembers() + for i = 1, 4 do + local data = self.frames[i] + local f = data.frame + if i <= numParty then + f:Show() + self:UpdateFrame(data.unit) + else + f:Hide() + end + end +end + +function SFrames.Party:UpdateFrame(unit) + if self.testing then return end + local data = self:GetFrameByUnit(unit) + if not data then return end + local f = data.frame + + f.portrait:SetUnit(unit) + f.portrait:SetCamera(0) + f.portrait:Hide() + f.portrait:Show() + + local name = UnitName(unit) or "" + local level = UnitLevel(unit) + if level == -1 then level = "??" end + + local _, class = UnitClass(unit) + + if not UnitIsConnected(unit) then + f.health:SetStatusBarColor(0.5, 0.5, 0.5) + f.nameText:SetText(name) + f.nameText:SetTextColor(0.5, 0.5, 0.5) + if f.offlineIcon then f.offlineIcon:Show() end + else + if f.offlineIcon then f.offlineIcon:Hide() end + if class and SFrames.Config.colors.class[class] then + local color = SFrames.Config.colors.class[class] + f.health:SetStatusBarColor(color.r, color.g, color.b) + f.nameText:SetText(level .. " " .. name) + f.nameText:SetTextColor(color.r, color.g, color.b) + else + f.health:SetStatusBarColor(0, 1, 0) + f.nameText:SetText(level .. " " .. name) + f.nameText:SetTextColor(1, 1, 1) + end + end + + -- Update Leader/Master Looter + if GetPartyLeaderIndex() == data.index then + f.leaderIcon:Show() + else + f.leaderIcon:Hide() + end + + local method, partyIndex, raidIndex = GetLootMethod() + if method == "master" and partyIndex == data.index then + f.masterIcon:Show() + else + f.masterIcon:Hide() + end + + self:UpdateHealth(unit) + self:UpdatePowerType(unit) + self:UpdatePower(unit) + self:UpdateAuras(unit) + self:UpdateRaidIcon(unit) + + local petUnit = string.gsub(unit, "party", "partypet") + self:UpdatePet(petUnit) +end + +function SFrames.Party:UpdatePortrait(unit) + local data = self:GetFrameByUnit(unit) + if not data then return end + local f = data.frame + f.portrait:SetUnit(unit) + f.portrait:SetCamera(0) + f.portrait:Hide() + f.portrait:Show() +end + +function SFrames.Party:UpdateHealth(unit) + local data = self:GetFrameByUnit(unit) + if not data then return end + local f = data.frame + + if not UnitIsConnected(unit) then + f.health:SetMinMaxValues(0, 100) + f.health:SetValue(0) + f.healthText:SetText("Offline") + if f.health.healPredMine then f.health.healPredMine:Hide() end + if f.health.healPredOther then f.health.healPredOther:Hide() end + if f.offlineIcon then f.offlineIcon:Show() end + return + end + if f.offlineIcon then f.offlineIcon:Hide() end + + local hp = UnitHealth(unit) + local maxHp = UnitHealthMax(unit) + f.health:SetMinMaxValues(0, maxHp) + f.health:SetValue(hp) + + if maxHp > 0 then + local percent = math.floor((hp / maxHp) * 100) + f.healthText:SetText(percent .. "%") + else + f.healthText:SetText("") + end + + self:UpdateHealPrediction(unit) +end + +function SFrames.Party:UpdateHealPrediction(unit) + local data = self:GetFrameByUnit(unit) + if not data then return end + local f = data.frame + if not (f.health and f.health.healPredMine and f.health.healPredOther) then return end + + local predMine = f.health.healPredMine + local predOther = f.health.healPredOther + + local function HidePredictions() + predMine:Hide() + predOther:Hide() + end + + if not UnitExists(unit) or not UnitIsConnected(unit) then + HidePredictions() + return + end + + local hp = UnitHealth(unit) or 0 + local maxHp = UnitHealthMax(unit) or 0 + if maxHp <= 0 or hp >= maxHp then + HidePredictions() + return + end + + local _, mineIncoming, othersIncoming = GetIncomingHeals(unit) + local missing = maxHp - hp + if missing <= 0 then + HidePredictions() + return + end + + local mineShown = math.min(math.max(0, mineIncoming), missing) + local remaining = missing - mineShown + local otherShown = math.min(math.max(0, othersIncoming), remaining) + if mineShown <= 0 and otherShown <= 0 then + HidePredictions() + return + end + + local barWidth = f.health:GetWidth() or 0 + if barWidth <= 0 then + HidePredictions() + return + end + + local currentWidth = math.floor((hp / maxHp) * barWidth + 0.5) + if currentWidth < 0 then currentWidth = 0 end + if currentWidth > barWidth then currentWidth = barWidth end + + local availableWidth = barWidth - currentWidth + if availableWidth <= 0 then + HidePredictions() + return + end + + local mineWidth = math.floor((mineShown / maxHp) * barWidth + 0.5) + local otherWidth = math.floor((otherShown / maxHp) * barWidth + 0.5) + if mineWidth < 0 then mineWidth = 0 end + if otherWidth < 0 then otherWidth = 0 end + if mineWidth > availableWidth then mineWidth = availableWidth end + if otherWidth > (availableWidth - mineWidth) then + otherWidth = availableWidth - mineWidth + end + + if mineWidth > 0 then + predMine:ClearAllPoints() + predMine:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth, 0) + predMine:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth, 0) + predMine:SetWidth(mineWidth) + predMine:Show() + else + predMine:Hide() + end + + if otherWidth > 0 then + predOther:ClearAllPoints() + predOther:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth + mineWidth, 0) + predOther:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth + mineWidth, 0) + predOther:SetWidth(otherWidth) + predOther:Show() + else + predOther:Hide() + end +end + +function SFrames.Party:UpdatePowerType(unit) + local data = self:GetFrameByUnit(unit) + if not data then return end + local f = data.frame + + local powerType = UnitPowerType(unit) + local color = SFrames.Config.colors.power[powerType] + if color then + f.power:SetStatusBarColor(color.r, color.g, color.b) + else + f.power:SetStatusBarColor(0, 0, 1) + end +end + +function SFrames.Party:UpdatePower(unit) + local data = self:GetFrameByUnit(unit) + if not data then return end + local f = data.frame + + if not UnitIsConnected(unit) then + f.power:SetMinMaxValues(0, 100) + f.power:SetValue(0) + return + end + + local power = UnitMana(unit) + local maxPower = UnitManaMax(unit) + f.power:SetMinMaxValues(0, maxPower) + f.power:SetValue(power) +end + +function SFrames.Party:UpdateRaidIcons() + for i = 1, 4 do + if self.frames[i] and self.frames[i].frame:IsShown() then + self:UpdateRaidIcon(self.frames[i].unit) + end + end +end + +function SFrames.Party:UpdateRaidIcon(unit) + local data = self:GetFrameByUnit(unit) + if not data then return end + local f = data.frame + if not f.raidIcon then return end + if not GetRaidTargetIndex then + f.raidIcon:Hide() + return + end + if not UnitExists(unit) then + f.raidIcon:Hide() + return + end + local index = GetRaidTargetIndex(unit) + if index and index > 0 and index <= 8 then + local col = math.mod(index - 1, 4) + local row = math.floor((index - 1) / 4) + f.raidIcon:SetTexCoord(col * 0.25, (col + 1) * 0.25, row * 0.25, (row + 1) * 0.25) + f.raidIcon:Show() + else + f.raidIcon:Hide() + end +end + +function SFrames.Party:UpdateAuras(unit) + local data = self:GetFrameByUnit(unit) + if not data then return end + local f = data.frame + + local showDebuffs = not (SFramesDB and SFramesDB.partyShowDebuffs == false) + local showBuffs = not (SFramesDB and SFramesDB.partyShowBuffs == false) + + local hasDebuff = false + local debuffColor = {r=_A.slotBg[1], g=_A.slotBg[2], b=_A.slotBg[3]} + + SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") + + -- Debuffs + if showDebuffs then + for i = 1, 4 do + local texture, applications, debuffType = UnitDebuff(unit, i) + local b = f.debuffs[i] + b:SetID(i) + + if texture then + if debuffType then + hasDebuff = true + if debuffType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1} + elseif debuffType == "Curse" then debuffColor = {r=0.6, g=0, b=1} + elseif debuffType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0} + elseif debuffType == "Poison" then debuffColor = {r=0, g=0.6, b=0} + end + end + + b.icon:SetTexture(texture) + + SFrames.Tooltip:ClearLines() + SFrames.Tooltip:SetUnitDebuff(unit, i) + local timeLeft = SFrames:GetAuraTimeLeft(unit, i, false) + if timeLeft and timeLeft > 0 then + local newExp = GetTime() + timeLeft + if not b.expirationTime or math.abs(b.expirationTime - newExp) > 2 then + b.expirationTime = newExp + end + local currentLeft = b.expirationTime - GetTime() + if currentLeft > 0 and currentLeft < 3600 then + b.cdText:SetText(SFrames:FormatTime(currentLeft)) + else + b.cdText:SetText("") + end + else + b.expirationTime = nil + b.cdText:SetText("") + end + + b:Show() + else + b.expirationTime = nil + b.cdText:SetText("") + b:Hide() + end + end + else + for i = 1, 4 do + local b = f.debuffs[i] + b.expirationTime = nil + b.cdText:SetText("") + b:Hide() + end + end + + if hasDebuff then + f.health.bg:SetVertexColor(debuffColor.r, debuffColor.g, debuffColor.b, 1) + else + f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + end + + -- Buffs + if showBuffs then + for i = 1, 4 do + local texture = UnitBuff(unit, i) + local b = f.buffs[i] + b:SetID(i) + + if texture then + b.icon:SetTexture(texture) + + SFrames.Tooltip:ClearLines() + SFrames.Tooltip:SetUnitBuff(unit, i) + local timeLeft = SFrames:GetAuraTimeLeft(unit, i, true) + if timeLeft and timeLeft > 0 then + local newExp = GetTime() + timeLeft + if not b.expirationTime or math.abs(b.expirationTime - newExp) > 2 then + b.expirationTime = newExp + end + local currentLeft = b.expirationTime - GetTime() + if currentLeft > 0 and currentLeft < 3600 then + b.cdText:SetText(SFrames:FormatTime(currentLeft)) + else + b.cdText:SetText("") + end + else + b.expirationTime = nil + b.cdText:SetText("") + end + + b:Show() + else + b.expirationTime = nil + b.cdText:SetText("") + b:Hide() + end + end + else + for i = 1, 4 do + local b = f.buffs[i] + b.expirationTime = nil + b.cdText:SetText("") + b:Hide() + end + end +end + +function SFrames.Party:TestMode() + self.testing = not self.testing + if self.testing then + for i = 1, 4 do + local data = self.frames[i] + local f = data.frame + f:Show() + + f.health:SetMinMaxValues(0, 100) + f.health:SetValue(math.random(30, 90)) + f.health:SetStatusBarColor(SFrames.Config.colors.class["DRUID"].r, SFrames.Config.colors.class["DRUID"].g, SFrames.Config.colors.class["DRUID"].b) + f.nameText:SetText("60 队友" .. i) + f.nameText:SetTextColor(SFrames.Config.colors.class["DRUID"].r, SFrames.Config.colors.class["DRUID"].g, SFrames.Config.colors.class["DRUID"].b) + f.healthText:SetText(math.floor(f.health:GetValue()) .. "%") + if f.health.healPredMine then f.health.healPredMine:Hide() end + if f.health.healPredOther then f.health.healPredOther:Hide() end + + f.power:SetMinMaxValues(0, 100) + f.power:SetValue(math.random(20, 100)) + f.power:SetStatusBarColor(SFrames.Config.colors.power[0].r, SFrames.Config.colors.power[0].g, SFrames.Config.colors.power[0].b) + + f.leaderIcon:Hide() + f.masterIcon:Hide() + if i == 1 then + f.leaderIcon:Show() + f.masterIcon:Show() + end + + -- Show one dummy debuff to test positioning + f.debuffs[1].icon:SetTexture("Interface\\Icons\\Spell_Shadow_ShadowWordPain") + f.debuffs[1]:Show() + + -- Test pet + if f.petFrame then + f.petFrame:Show() + f.petFrame.nameText:SetText("测试宠物") + f.petFrame.health:SetMinMaxValues(0, 100) + f.petFrame.health:SetValue(100) + f.petFrame.health:SetStatusBarColor(0.2, 0.8, 0.2) + end + end + else + self:UpdateAll() + for i = 1, 4 do + self.frames[i].frame.debuffs[1]:Hide() + end + end +end diff --git a/Units/Pet.lua b/Units/Pet.lua new file mode 100644 index 0000000..eee161a --- /dev/null +++ b/Units/Pet.lua @@ -0,0 +1,787 @@ +SFrames.Pet = {} +local _A = SFrames.ActiveTheme + +local function Clamp(value, minValue, maxValue) + if value < minValue then return minValue end + if value > maxValue then return maxValue end + return value +end + +function SFrames.Pet:Initialize() + local f = CreateFrame("Button", "SFramesPetFrame", UIParent) + f:SetWidth(150) + f:SetHeight(30) + + if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PetFrame"] then + local pos = SFramesDB.Positions["PetFrame"] + f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs) + else + f:SetPoint("TOPLEFT", SFramesPlayerFrame, "BOTTOMLEFT", 10, -55) + end + + local frameScale = (SFramesDB and type(SFramesDB.petFrameScale) == "number") and SFramesDB.petFrameScale or 1 + f:SetScale(Clamp(frameScale, 0.7, 1.8)) + + f:SetMovable(true) + f:EnableMouse(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() if IsAltKeyDown() or SFrames.isUnlocked then f:StartMoving() end end) + f:SetScript("OnDragStop", function() + f:StopMovingOrSizing() + if not SFramesDB then SFramesDB = {} end + if not SFramesDB.Positions then SFramesDB.Positions = {} end + local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint() + SFramesDB.Positions["PetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs } + end) + + f:RegisterForClicks("LeftButtonUp", "RightButtonUp") + f:SetScript("OnClick", function() + if arg1 == "LeftButton" then + if SpellIsTargeting() then + SpellTargetUnit("pet") + elseif CursorHasItem() then + DropItemOnUnit("pet") + else + TargetUnit("pet") + end + else + ToggleDropDownMenu(1, nil, PetFrameDropDown, "SFramesPetFrame", 106, 27) + end + end) + + f:SetScript("OnReceiveDrag", function() + if CursorHasItem() then + DropItemOnUnit("pet") + end + end) + + f:SetScript("OnEnter", function() + GameTooltip_SetDefaultAnchor(GameTooltip, this) + GameTooltip:SetUnit("pet") + GameTooltip:Show() + end) + f:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + SFrames:CreateUnitBackdrop(f) + + -- Health Bar + f.health = SFrames:CreateStatusBar(f, "SFramesPetHealth") + f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) + f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) + f.health:SetHeight(18) + + local hbg = CreateFrame("Frame", nil, f) + hbg:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1) + hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) + hbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(hbg) + + f.health.bg = f.health:CreateTexture(nil, "BACKGROUND") + f.health.bg:SetAllPoints() + f.health.bg:SetTexture(SFrames:GetTexture()) + f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + -- Power Bar + f.power = SFrames:CreateStatusBar(f, "SFramesPetPower") + f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) + f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) + + local pbg = CreateFrame("Frame", nil, f) + pbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1) + pbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) + pbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(pbg) + + f.power.bg = f.power:CreateTexture(nil, "BACKGROUND") + f.power.bg:SetAllPoints() + f.power.bg:SetTexture(SFrames:GetTexture()) + f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + -- Texts + local fontPath = SFrames:GetFont() + local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" + + f.nameText = SFrames:CreateFontString(f.health, 10, "LEFT") + f.nameText:SetPoint("LEFT", f.health, "LEFT", 4, 0) + f.nameText:SetWidth(75) + f.nameText:SetHeight(12) + f.nameText:SetJustifyH("LEFT") + f.nameText:SetFont(fontPath, 10, outline) + f.nameText:SetShadowColor(0, 0, 0, 1) + f.nameText:SetShadowOffset(1, -1) + + f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT") + f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0) + f.healthText:SetFont(fontPath, 10, outline) + f.healthText:SetShadowColor(0, 0, 0, 1) + f.healthText:SetShadowOffset(1, -1) + + -- Happiness Icon (for hunters) + local hBG = CreateFrame("Frame", nil, f) + hBG:SetWidth(20) + hBG:SetHeight(20) + hBG:SetPoint("RIGHT", f, "LEFT", -2, 0) + SFrames:CreateUnitBackdrop(hBG) + + f.happiness = hBG:CreateTexture(nil, "OVERLAY") + f.happiness:SetPoint("TOPLEFT", hBG, "TOPLEFT", 1, -1) + f.happiness:SetPoint("BOTTOMRIGHT", hBG, "BOTTOMRIGHT", -1, 1) + f.happiness:SetTexture("Interface\\PetPaperDollFrame\\UI-PetHappiness") + + f.happinessBG = hBG + f.happinessBG:Hide() + + self.frame = f + self.frame.unit = "pet" + f:Hide() + + SFrames:RegisterEvent("UNIT_PET", function() if arg1 == "player" then self:UpdateAll() end end) + SFrames:RegisterEvent("PET_BAR_UPDATE", function() self:UpdateAll() end) + SFrames:RegisterEvent("UNIT_HEALTH", function() if arg1 == "pet" then self:UpdateHealth() end end) + SFrames:RegisterEvent("UNIT_MAXHEALTH", function() if arg1 == "pet" then self:UpdateHealth() end end) + SFrames:RegisterEvent("UNIT_MANA", function() if arg1 == "pet" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_MAXMANA", function() if arg1 == "pet" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_ENERGY", function() if arg1 == "pet" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_MAXENERGY", function() if arg1 == "pet" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_FOCUS", function() if arg1 == "pet" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_MAXFOCUS", function() if arg1 == "pet" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_RAGE", function() if arg1 == "pet" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_MAXRAGE", function() if arg1 == "pet" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if arg1 == "pet" then self:UpdatePowerType() end end) + SFrames:RegisterEvent("UNIT_HAPPINESS", function() if arg1 == "pet" then self:UpdateHappiness() end end) + SFrames:RegisterEvent("UNIT_NAME_UPDATE", function() if arg1 == "pet" then self:UpdateAll() end end) + SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:UpdateAll() end) + + self:InitFoodFeature() + self:UpdateAll() +end + +function SFrames.Pet:UpdateAll() + if UnitExists("pet") then + if SFramesDB and SFramesDB.showPetFrame == false then + self.frame:Hide() + if self.foodPanel then self.foodPanel:Hide() end + return + end + + self.frame:Show() + self:UpdateHealth() + self:UpdatePowerType() + self:UpdatePower() + self:UpdateHappiness() + + local name = UnitName("pet") + if name == UNKNOWNOBJECT or name == "未知目标" or name == "Unknown" then + name = "宠物" + end + self.frame.nameText:SetText(name) + + local r, g, b = 0.33, 0.59, 0.33 + self.frame.health:SetStatusBarColor(r, g, b) + else + self.frame:Hide() + if self.foodPanel then self.foodPanel:Hide() end + end +end + +function SFrames.Pet:UpdateHealth() + local hp = UnitHealth("pet") + local maxHp = UnitHealthMax("pet") + self.frame.health:SetMinMaxValues(0, maxHp) + self.frame.health:SetValue(hp) + + if maxHp > 0 then + self.frame.healthText:SetText(hp .. " / " .. maxHp) + else + self.frame.healthText:SetText("") + end +end + +function SFrames.Pet:UpdatePowerType() + local powerType = UnitPowerType("pet") + local color = SFrames.Config.colors.power[powerType] + if color then + self.frame.power:SetStatusBarColor(color.r, color.g, color.b) + else + self.frame.power:SetStatusBarColor(0, 0, 1) + end +end + +function SFrames.Pet:UpdatePower() + local power = UnitMana("pet") + local maxPower = UnitManaMax("pet") + self.frame.power:SetMinMaxValues(0, maxPower) + self.frame.power:SetValue(power) +end + +function SFrames.Pet:UpdateHappiness() + local happiness = GetPetHappiness() + if not happiness then + self.frame.happinessBG:Hide() + self:UpdateFoodButton() + return + end + + local isHunter = false + local _, class = UnitClass("player") + if class == "HUNTER" then isHunter = true end + + if isHunter then + if happiness == 1 then + self.frame.happiness:SetTexCoord(0.375, 0.5625, 0, 0.359375) + self.frame.happinessBG:Show() + elseif happiness == 2 then + self.frame.happiness:SetTexCoord(0.1875, 0.375, 0, 0.359375) + self.frame.happinessBG:Show() + elseif happiness == 3 then + self.frame.happiness:SetTexCoord(0, 0.1875, 0, 0.359375) + self.frame.happinessBG:Show() + end + else + self.frame.happinessBG:Hide() + end + self:UpdateFoodButton() +end + +-------------------------------------------------------------------------------- +-- Pet Food Feature (Hunter only) +-- Food button on pet frame, food selection panel, quick-feed via right-click +-------------------------------------------------------------------------------- + +local petFoodScanTip +local cachedFeedSpell + +local function EnsureFoodScanTooltip() + if not petFoodScanTip then + petFoodScanTip = CreateFrame("GameTooltip", "NanamiPetFoodScanTip", UIParent, "GameTooltipTemplate") + petFoodScanTip:SetOwner(UIParent, "ANCHOR_NONE") + end + return petFoodScanTip +end + +local function GetFeedPetSpell() + if cachedFeedSpell then return cachedFeedSpell end + for tab = 1, GetNumSpellTabs() do + local _, _, offset, numSpells = GetSpellTabInfo(tab) + for i = offset + 1, offset + numSpells do + local spellName = GetSpellName(i, BOOKTYPE_SPELL) + if spellName and (spellName == "Feed Pet" or spellName == "喂养宠物") then + cachedFeedSpell = spellName + return cachedFeedSpell + end + end + end + cachedFeedSpell = "Feed Pet" + return cachedFeedSpell +end + +local REJECT_NAME_PATTERNS = { + "Potion", "potion", "药水", + "Elixir", "elixir", "药剂", + "Flask", "flask", "合剂", + "Bandage", "bandage", "绷带", + "Scroll", "scroll", "卷轴", + "Healthstone", "healthstone", "治疗石", + "Mana Gem", "法力宝石", + "Thistle Tea", "蓟花茶", + "Firewater", "火焰花水", + "Juju", "符咒", +} + +local function NameIsRejected(itemName) + for i = 1, table.getn(REJECT_NAME_PATTERNS) do + if string.find(itemName, REJECT_NAME_PATTERNS[i], 1, true) then + return true + end + end + return false +end + +local function IsItemPetFood(bag, slot) + local link = GetContainerItemLink(bag, slot) + if not link then return false end + + local texture = GetContainerItemInfo(bag, slot) + + local _, _, itemIdStr = string.find(link, "item:(%d+)") + local name, itemType, subType + + if itemIdStr then + local n, _, _, _, _, t, st = GetItemInfo("item:" .. itemIdStr) + name = n + itemType = t + subType = st + end + + if not name then + local _, _, parsed = string.find(link, "%[(.+)%]") + name = parsed + end + if not name then return false end + + if NameIsRejected(name) then + return false + end + + if itemType then + if itemType ~= "Consumable" and itemType ~= "消耗品" then + return false + end + if subType then + if string.find(subType, "Potion", 1, true) or string.find(subType, "药水", 1, true) + or string.find(subType, "Elixir", 1, true) or string.find(subType, "药剂", 1, true) + or string.find(subType, "Flask", 1, true) or string.find(subType, "合剂", 1, true) + or string.find(subType, "Bandage", 1, true) or string.find(subType, "绷带", 1, true) + or string.find(subType, "Scroll", 1, true) or string.find(subType, "卷轴", 1, true) then + return false + end + if string.find(subType, "Food", 1, true) or string.find(subType, "食物", 1, true) then + return true, name, texture + end + end + end + + local tip = EnsureFoodScanTooltip() + tip:SetOwner(UIParent, "ANCHOR_NONE") + tip:SetBagItem(bag, slot) + + local found = false + local rejected = false + for i = 1, tip:NumLines() do + local leftObj = _G["NanamiPetFoodScanTipTextLeft" .. i] + if leftObj then + local text = leftObj:GetText() + if text then + if string.find(text, "进食", 1, true) or string.find(text, "eating", 1, true) then + found = true + end + if string.find(text, "Well Fed", 1, true) or string.find(text, "充分进食", 1, true) then + found = true + end + if string.find(text, "Restores", 1, true) and string.find(text, "health", 1, true) + and string.find(text, "over", 1, true) then + found = true + end + if string.find(text, "恢复", 1, true) and string.find(text, "生命", 1, true) + and string.find(text, "秒", 1, true) then + found = true + end + if string.find(text, "Potion", 1, true) or string.find(text, "药水", 1, true) + or string.find(text, "Elixir", 1, true) or string.find(text, "药剂", 1, true) + or string.find(text, "Bandage", 1, true) or string.find(text, "绷带", 1, true) then + rejected = true + end + end + end + end + tip:Hide() + + if found and not rejected then + return true, name, texture + end + + if itemType and subType then + if (itemType == "Consumable" or itemType == "消耗品") + and (subType == "Food & Drink" or subType == "食物和饮料") then + return true, name, texture + end + end + + return false +end + +function SFrames.Pet:ScanBagsForFood() + local foods = {} + for bag = 0, 4 do + for slot = 1, GetContainerNumSlots(bag) do + local isFood, name, itemTex = IsItemPetFood(bag, slot) + if isFood then + local texture, itemCount = GetContainerItemInfo(bag, slot) + local link = GetContainerItemLink(bag, slot) + table.insert(foods, { + bag = bag, + slot = slot, + name = name, + link = link, + texture = texture or itemTex, + count = itemCount or 1, + }) + end + end + end + return foods +end + +function SFrames.Pet:FeedPet(bag, slot) + if not UnitExists("pet") then return end + local link = GetContainerItemLink(bag, slot) + local tex = GetContainerItemInfo(bag, slot) + local spell = GetFeedPetSpell() + CastSpellByName(spell) + PickupContainerItem(bag, slot) + if link then + local _, _, itemId = string.find(link, "item:(%d+)") + if itemId then + if not SFramesDB then SFramesDB = {} end + SFramesDB.lastPetFoodId = tonumber(itemId) + end + end + if tex and self.foodButton then + self.foodButton.icon:SetTexture(tex) + if not SFramesDB then SFramesDB = {} end + SFramesDB.lastPetFoodIcon = tex + end +end + +function SFrames.Pet:QuickFeed() + if not UnitExists("pet") then return end + local foods = self:ScanBagsForFood() + if table.getn(foods) == 0 then + DEFAULT_CHAT_FRAME:AddMessage("|cffff9900[Nanami]|r 背包中没有可喂食的食物") + return + end + local preferred = SFramesDB and SFramesDB.lastPetFoodId + if preferred then + for i = 1, table.getn(foods) do + local f = foods[i] + if f.link then + local _, _, itemId = string.find(f.link, "item:(%d+)") + if itemId and tonumber(itemId) == preferred then + self:FeedPet(f.bag, f.slot) + return + end + end + end + end + self:FeedPet(foods[1].bag, foods[1].slot) +end + +-------------------------------------------------------------------------------- +-- Food Button & Panel UI +-------------------------------------------------------------------------------- + +local FOOD_COLS = 6 +local FOOD_SLOT_SIZE = 30 +local FOOD_SLOT_GAP = 2 +local FOOD_PAD = 8 + +function SFrames.Pet:CreateFoodButton() + local f = self.frame + local A = SFrames.ActiveTheme + + local btn = CreateFrame("Button", "SFramesPetFoodBtn", f) + btn:SetWidth(20) + btn:SetHeight(20) + btn:SetPoint("TOP", f.happinessBG, "BOTTOM", 0, -2) + SFrames:CreateUnitBackdrop(btn) + + local icon = btn:CreateTexture(nil, "ARTWORK") + icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 1, -1) + icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -1, 1) + icon:SetTexture("Interface\\Icons\\INV_Misc_Food_14") + btn.icon = icon + + btn:RegisterForClicks("LeftButtonUp", "RightButtonUp") + + local pet = self + btn:SetScript("OnClick", function() + if CursorHasItem() then + local curTex = pet:GetCursorItemTexture() + DropItemOnUnit("pet") + if curTex then + pet:SetFoodIcon(curTex) + end + return + end + if arg1 == "RightButton" then + pet:QuickFeed() + else + if pet.foodPanel and pet.foodPanel:IsShown() then + pet.foodPanel:Hide() + else + pet:ShowFoodPanel() + end + end + end) + + btn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:AddLine("喂养宠物", 1, 1, 1) + GameTooltip:AddLine("左键: 选择食物", 0.7, 0.7, 0.7) + GameTooltip:AddLine("右键: 快速喂食", 0.7, 0.7, 0.7) + GameTooltip:AddLine("可拖拽背包食物到此按钮", 0.7, 0.7, 0.7) + GameTooltip:Show() + end) + + btn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + btn:SetScript("OnReceiveDrag", function() + if CursorHasItem() then + DropItemOnUnit("pet") + end + end) + + self.foodButton = btn + btn:Hide() +end + +function SFrames.Pet:CreateFoodPanel() + if self.foodPanel then return self.foodPanel end + + local A = SFrames.ActiveTheme + + local panel = CreateFrame("Frame", "SFramesPetFoodPanel", UIParent) + panel:SetFrameStrata("DIALOG") + panel:SetFrameLevel(20) + panel:SetWidth(FOOD_COLS * (FOOD_SLOT_SIZE + FOOD_SLOT_GAP) + FOOD_PAD * 2 - FOOD_SLOT_GAP) + panel:SetHeight(80) + panel:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", -22, 4) + SFrames:CreateUnitBackdrop(panel) + panel:EnableMouse(true) + panel:Hide() + + local titleBar = CreateFrame("Frame", nil, panel) + titleBar:SetPoint("TOPLEFT", panel, "TOPLEFT", 1, -1) + titleBar:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -1, -1) + titleBar:SetHeight(18) + titleBar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) + local hdr = A.headerBg or A.panelBg + titleBar:SetBackdropColor(hdr[1], hdr[2], hdr[3], (hdr[4] or 0.9) * 0.6) + + local titleText = titleBar:CreateFontString(nil, "OVERLAY") + titleText:SetFont(SFrames:GetFont(), 10, SFrames.Media.fontOutline or "OUTLINE") + titleText:SetPoint("LEFT", titleBar, "LEFT", FOOD_PAD, 0) + titleText:SetTextColor(A.title[1], A.title[2], A.title[3]) + titleText:SetText("选择食物") + panel.titleText = titleText + + local hintText = panel:CreateFontString(nil, "OVERLAY") + hintText:SetFont(SFrames:GetFont(), 9, SFrames.Media.fontOutline or "OUTLINE") + hintText:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", FOOD_PAD, 4) + hintText:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -FOOD_PAD, 4) + hintText:SetJustifyH("LEFT") + local dim = A.dimText or { 0.5, 0.5, 0.5 } + hintText:SetTextColor(dim[1], dim[2], dim[3]) + hintText:SetText("点击喂食 | 可拖拽食物到此面板") + panel.hintText = hintText + + local emptyText = panel:CreateFontString(nil, "OVERLAY") + emptyText:SetFont(SFrames:GetFont(), 10, SFrames.Media.fontOutline or "OUTLINE") + emptyText:SetPoint("CENTER", panel, "CENTER", 0, 0) + emptyText:SetTextColor(dim[1], dim[2], dim[3]) + emptyText:SetText("背包中没有可喂食的食物") + emptyText:Hide() + panel.emptyText = emptyText + + local pet = self + panel:SetScript("OnReceiveDrag", function() + if CursorHasItem() then + local curTex = pet:GetCursorItemTexture() + DropItemOnUnit("pet") + if curTex then + pet:SetFoodIcon(curTex) + end + end + end) + + table.insert(UISpecialFrames, "SFramesPetFoodPanel") + + panel.slots = {} + self.foodPanel = panel + return panel +end + +function SFrames.Pet:CreateFoodSlot(parent, index) + local A = SFrames.ActiveTheme + + local slot = CreateFrame("Button", "SFramesPetFoodSlot" .. index, parent) + slot:SetWidth(FOOD_SLOT_SIZE) + slot:SetHeight(FOOD_SLOT_SIZE) + slot:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 } + }) + slot:SetBackdropColor(A.slotBg[1], A.slotBg[2], A.slotBg[3], A.slotBg[4] or 0.9) + slot:SetBackdropBorderColor(0, 0, 0, 1) + + local icon = slot:CreateTexture(nil, "ARTWORK") + icon:SetPoint("TOPLEFT", 2, -2) + icon:SetPoint("BOTTOMRIGHT", -2, 2) + slot.icon = icon + + local count = slot:CreateFontString(nil, "OVERLAY") + count:SetFont(SFrames:GetFont(), 10, "OUTLINE") + count:SetPoint("BOTTOMRIGHT", -2, 2) + count:SetJustifyH("RIGHT") + count:SetTextColor(1, 1, 1) + slot.count = count + + slot:RegisterForClicks("LeftButtonUp", "RightButtonUp") + + local pet = self + slot:SetScript("OnClick", function() + if CursorHasItem() then + local curTex = pet:GetCursorItemTexture() + DropItemOnUnit("pet") + if curTex then + pet:SetFoodIcon(curTex) + end + return + end + if IsShiftKeyDown() and this.foodLink then + if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then + ChatFrameEditBox:Insert(this.foodLink) + end + return + end + if this.foodBag and this.foodSlot then + pet:FeedPet(this.foodBag, this.foodSlot) + if pet.foodPanel then pet.foodPanel:Hide() end + end + end) + + slot:SetScript("OnEnter", function() + if this.foodBag and this.foodSlot then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetBagItem(this.foodBag, this.foodSlot) + GameTooltip:AddLine(" ") + GameTooltip:AddLine("点击: 喂食宠物", 0.5, 1, 0.5) + GameTooltip:AddLine("Shift+点击: 链接到聊天", 0.7, 0.7, 0.7) + GameTooltip:Show() + end + this:SetBackdropBorderColor(0.4, 0.4, 0.4, 1) + end) + + slot:SetScript("OnLeave", function() + GameTooltip:Hide() + this:SetBackdropBorderColor(0, 0, 0, 1) + end) + + slot:SetScript("OnReceiveDrag", function() + if CursorHasItem() then + DropItemOnUnit("pet") + end + end) + + return slot +end + +function SFrames.Pet:ShowFoodPanel() + self:CreateFoodPanel() + self:RefreshFoodPanel() + self.foodPanel:Show() +end + +function SFrames.Pet:RefreshFoodPanel() + local panel = self.foodPanel + if not panel then return end + + local foods = self:ScanBagsForFood() + local numFoods = table.getn(foods) + + for i = 1, table.getn(panel.slots) do + panel.slots[i]:Hide() + end + + if numFoods == 0 then + panel:SetHeight(60) + panel.emptyText:Show() + panel.hintText:Hide() + return + end + + panel.emptyText:Hide() + panel.hintText:Show() + + local rows = math.ceil(numFoods / FOOD_COLS) + local panelH = FOOD_PAD + 20 + rows * (FOOD_SLOT_SIZE + FOOD_SLOT_GAP) + 18 + panel:SetHeight(panelH) + + for i = 1, numFoods do + local food = foods[i] + local slot = panel.slots[i] + if not slot then + slot = self:CreateFoodSlot(panel, i) + panel.slots[i] = slot + end + + local col = mod(i - 1, FOOD_COLS) + local row = math.floor((i - 1) / FOOD_COLS) + + slot:ClearAllPoints() + slot:SetPoint("TOPLEFT", panel, "TOPLEFT", + FOOD_PAD + col * (FOOD_SLOT_SIZE + FOOD_SLOT_GAP), + -(FOOD_PAD + 18 + row * (FOOD_SLOT_SIZE + FOOD_SLOT_GAP))) + + slot.icon:SetTexture(food.texture) + slot.count:SetText(food.count > 1 and tostring(food.count) or "") + slot.foodBag = food.bag + slot.foodSlot = food.slot + slot.foodName = food.name + slot.foodLink = food.link + slot:Show() + end +end + +function SFrames.Pet:GetCursorItemTexture() + for bag = 0, 4 do + for slot = 1, GetContainerNumSlots(bag) do + local texture, count, locked = GetContainerItemInfo(bag, slot) + if locked and texture then + return texture + end + end + end + return nil +end + +function SFrames.Pet:SetFoodIcon(tex) + if not tex or not self.foodButton then return end + self.foodButton.icon:SetTexture(tex) + if not SFramesDB then SFramesDB = {} end + SFramesDB.lastPetFoodIcon = tex +end + +function SFrames.Pet:InitFoodFeature() + local _, playerClass = UnitClass("player") + if playerClass ~= "HUNTER" then return end + + self:CreateFoodButton() + + if SFramesDB and SFramesDB.lastPetFoodIcon then + self.foodButton.icon:SetTexture(SFramesDB.lastPetFoodIcon) + end + + local pet = self + SFrames:RegisterEvent("BAG_UPDATE", function() + if pet.foodPanel and pet.foodPanel:IsShown() then + pet:RefreshFoodPanel() + end + end) +end + +function SFrames.Pet:UpdateFoodButton() + if not self.foodButton then return end + + local _, class = UnitClass("player") + if class == "HUNTER" and UnitExists("pet") then + self.foodButton:Show() + local happiness = GetPetHappiness() + if happiness and happiness == 1 then + self.foodButton.icon:SetVertexColor(1, 0.3, 0.3) + elseif happiness and happiness == 2 then + self.foodButton.icon:SetVertexColor(1, 0.8, 0.4) + else + self.foodButton.icon:SetVertexColor(1, 1, 1) + end + else + self.foodButton:Hide() + if self.foodPanel then self.foodPanel:Hide() end + end +end diff --git a/Units/Player.lua b/Units/Player.lua new file mode 100644 index 0000000..8515e17 --- /dev/null +++ b/Units/Player.lua @@ -0,0 +1,1449 @@ +SFrames.Player = {} +local _A = SFrames.ActiveTheme + +local CLASS_NAME_ZH = { + WARRIOR = "\230\136\152\229\163\171", + MAGE = "\230\179\149\229\184\136", + ROGUE = "\230\189\156\232\161\140\232\128\133", + DRUID = "\229\190\183\233\178\129\228\188\138", + HUNTER = "\231\140\142\228\186\186", + SHAMAN = "\232\144\168\230\187\161\231\165\173\229\143\184", + PRIEST = "\231\137\167\229\184\136", + WARLOCK = "\230\156\175\229\163\171", + PALADIN = "\229\156\163\233\170\145\229\163\171", +} + +local function GetChineseClassName(classToken, localizedClass) + if classToken and CLASS_NAME_ZH[classToken] then + return CLASS_NAME_ZH[classToken] + end + return localizedClass or "" +end + +local function GetIncomingHeals(unit) + if not (ShaguTweaks and ShaguTweaks.libpredict and ShaguTweaks.libpredict.UnitGetIncomingHeals) then + return 0, 0, 0 + end + + local libpredict = ShaguTweaks.libpredict + if libpredict.UnitGetIncomingHealsBreakdown then + local ok, total, mine, others = pcall(function() + return libpredict:UnitGetIncomingHealsBreakdown(unit, UnitName("player")) + end) + if ok then + total = math.max(0, tonumber(total) or 0) + mine = math.max(0, tonumber(mine) or 0) + others = math.max(0, tonumber(others) or 0) + return total, mine, others + end + end + + local ok, amount = pcall(function() + return libpredict:UnitGetIncomingHeals(unit) + end) + if not ok then return 0, 0, 0 end + + amount = tonumber(amount) or 0 + if amount < 0 then amount = 0 end + return amount, 0, amount +end + +local function Clamp(value, minValue, maxValue) + if value < minValue then + return minValue + end + if value > maxValue then + return maxValue + end + return value +end + +function SFrames.Player:GetConfig() + local db = SFramesDB or {} + + local width = tonumber(db.playerFrameWidth) or SFrames.Config.width or 220 + width = Clamp(math.floor(width + 0.5), 170, 420) + + local portraitWidth = tonumber(db.playerPortraitWidth) or SFrames.Config.portraitWidth or 50 + portraitWidth = Clamp(math.floor(portraitWidth + 0.5), 32, 95) + if portraitWidth > width - 90 then + portraitWidth = width - 90 + end + + local healthHeight = tonumber(db.playerHealthHeight) or 38 + healthHeight = Clamp(math.floor(healthHeight + 0.5), 14, 80) + + local powerHeight = tonumber(db.playerPowerHeight) or 9 + powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, 40) + + local height = healthHeight + powerHeight + 4 + height = Clamp(height, 30, 140) + + local nameFont = tonumber(db.playerNameFontSize) or 10 + nameFont = Clamp(math.floor(nameFont + 0.5), 8, 18) + + local valueFont = tonumber(db.playerValueFontSize) or 10 + valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18) + + local frameScale = tonumber(db.playerFrameScale) or 1 + frameScale = Clamp(frameScale, 0.7, 1.8) + + return { + width = width, + height = height, + portraitWidth = portraitWidth, + healthHeight = healthHeight, + powerHeight = powerHeight, + nameFont = nameFont, + valueFont = valueFont, + scale = frameScale, + } +end + +function SFrames.Player:ApplyConfig() + if not self.frame then return end + + local cfg = self:GetConfig() + local f = self.frame + + f:SetScale(cfg.scale) + f:SetWidth(cfg.width) + f:SetHeight(cfg.height) + + if f.portrait then + f.portrait:SetWidth(cfg.portraitWidth) + f.portrait:SetHeight(cfg.height - 2) + end + + if f.portraitBG then + f.portraitBG:ClearAllPoints() + f.portraitBG:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) + f.portraitBG:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMRIGHT", 1, -1) + end + + if f.health then + f.health:ClearAllPoints() + f.health:SetPoint("TOPLEFT", f.portrait, "TOPRIGHT", 1, 0) + f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) + f.health:SetHeight(cfg.healthHeight) + end + + if f.healthBGFrame then + f.healthBGFrame:ClearAllPoints() + f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1) + f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) + end + + if f.power then + f.power:ClearAllPoints() + f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) + f.power:SetPoint("TOPRIGHT", f.health, "BOTTOMRIGHT", 0, 0) + f.power:SetHeight(cfg.powerHeight) + end + + if f.powerBGFrame then + f.powerBGFrame:ClearAllPoints() + f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1) + f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) + end + + local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" + local fontPath = SFrames:GetFont() + + if f.nameText then + f.nameText:SetFont(fontPath, cfg.nameFont, outline) + end + if f.healthText then + f.healthText:SetFont(fontPath, cfg.valueFont, outline) + end + if f.powerText then + f.powerText:SetFont(fontPath, cfg.valueFont, outline) + end + if f.manaText then + local manaFont = cfg.valueFont - 1 + if manaFont < 8 then manaFont = 8 end + f.manaText:SetFont(fontPath, manaFont, outline) + end + + if f.zLetters then + for i = 1, 3 do + if f.zLetters[i] and f.zLetters[i].text then + f.zLetters[i].text:SetFont(fontPath, 8 + (i - 1) * 3, "OUTLINE") + end + end + end + + self:UpdateAll() +end + +function SFrames.Player:Initialize() + local f = CreateFrame("Button", "SFramesPlayerFrame", UIParent) + f:SetWidth(SFrames.Config.width) + f:SetHeight(SFrames.Config.height) + if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PlayerFrame"] then + local pos = SFramesDB.Positions["PlayerFrame"] + f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs) + else + f:SetPoint("CENTER", UIParent, "CENTER", -200, -100) + end + local frameScale = (SFramesDB and type(SFramesDB.playerFrameScale) == "number") and SFramesDB.playerFrameScale or 1 + f:SetScale(frameScale) + + -- Make it movable + f:SetMovable(true) + f:EnableMouse(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() if IsAltKeyDown() or SFrames.isUnlocked then f:StartMoving() end end) + f:SetScript("OnDragStop", function() + f:StopMovingOrSizing() + if not SFramesDB then SFramesDB = {} end + if not SFramesDB.Positions then SFramesDB.Positions = {} end + local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint() + SFramesDB.Positions["PlayerFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs } + end) + + -- Register clicks for targeting + f:RegisterForClicks("LeftButtonUp", "RightButtonUp") + f:SetScript("OnClick", function() + if arg1 == "LeftButton" then + TargetUnit("player") + else + ToggleDropDownMenu(1, nil, PlayerFrameDropDown, this:GetName(), 106, 27) + end + end) + + -- Base Backdrop + SFrames:CreateUnitBackdrop(f) + + -- 3D Portrait + local pWidth = SFrames.Config.portraitWidth + f.portrait = CreateFrame("PlayerModel", nil, f) + f.portrait:SetWidth(pWidth) + f.portrait:SetHeight(SFrames.Config.height - 2) + f.portrait:SetPoint("LEFT", f, "LEFT", 1, 0) + f.portrait:SetUnit("player") + f.portrait:SetCamera(0) + f.portrait:SetPosition(-1.0, 0, 0) + + -- We need a backdrop for the portrait to separate it from health bar + local pbg = CreateFrame("Frame", nil, f) + pbg:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) + pbg:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMRIGHT", 1, -1) + pbg:SetFrameLevel(f:GetFrameLevel()) + SFrames:CreateUnitBackdrop(pbg) + f.portraitBG = pbg + + -- Health Bar + f.health = SFrames:CreateStatusBar(f, "SFramesPlayerHealth") + f.health:SetPoint("TOPLEFT", f.portrait, "TOPRIGHT", 1, 0) + f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) + f.health:SetHeight((SFrames.Config.height - 2) * 0.82 - 1) -- 82% height, minus 1px gap + f.health:SetMinMaxValues(0, 100) + + -- Health Backdrop + local hbg = CreateFrame("Frame", nil, f) + hbg:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1) + hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) + hbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(hbg) + f.healthBGFrame = hbg + + -- Add a dark backdrop behind the health texture + f.health.bg = f.health:CreateTexture(nil, "BACKGROUND") + f.health.bg:SetAllPoints() + f.health.bg:SetTexture(SFrames:GetTexture()) + f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + -- Heal prediction overlay (incoming heals) + f.health.healPredMine = f.health:CreateTexture(nil, "OVERLAY") + f.health.healPredMine:SetTexture(SFrames:GetTexture()) + f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78) + f.health.healPredMine:Hide() + + f.health.healPredOther = f.health:CreateTexture(nil, "OVERLAY") + f.health.healPredOther:SetTexture(SFrames:GetTexture()) + f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5) + f.health.healPredOther:Hide() + + -- Power Bar + f.power = SFrames:CreateStatusBar(f, "SFramesPlayerPower") + f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) + f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) + f.power:SetMinMaxValues(0, 100) + + -- Power Backdrop + local powerbg = CreateFrame("Frame", nil, f) + powerbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1) + powerbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) + powerbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(powerbg) + f.powerBGFrame = powerbg + + -- Add a dark backdrop behind the power texture + f.power.bg = f.power:CreateTexture(nil, "BACKGROUND") + f.power.bg:SetAllPoints() + f.power.bg:SetTexture(SFrames:GetTexture()) + f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + -- Five-second rule ticker (mana regen delay indicator) + f.power.fsrGlow = f.power:CreateTexture(nil, "OVERLAY") + f.power.fsrGlow:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark") + f.power.fsrGlow:SetVertexColor(0.62, 0.90, 1.0, 0.52) + f.power.fsrGlow:SetBlendMode("ADD") + pcall(function() f.power.fsrGlow:SetDrawLayer("OVERLAY", 5) end) + f.power.fsrGlow:Hide() + + -- Texts + f.nameText = SFrames:CreateFontString(f.health, 10, "LEFT") + f.nameText:SetPoint("LEFT", f.health, "LEFT", 6, 0) + + f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT") + f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -6, 0) + + f.powerText = SFrames:CreateFontString(f.power, 10, "RIGHT") + f.powerText:SetPoint("RIGHT", f.power, "RIGHT", -6, 0) + + -- Extra mana text for shapeshift druids (show blue mana while rage/energy is active) + f.manaText = SFrames:CreateFontString(f.power, 9, "LEFT") + f.manaText:SetPoint("LEFT", f.power, "LEFT", 6, 0) + f.manaText:SetTextColor(0.30, 0.65, 1.0) + f.manaText:Hide() + + f.manaBar = CreateFrame("StatusBar", nil, f) + f.manaBar:SetHeight(4) + f.manaBar:SetPoint("TOPLEFT", f.power, "BOTTOMLEFT", 0, -1) + f.manaBar:SetPoint("TOPRIGHT", f.power, "BOTTOMRIGHT", 0, -1) + f.manaBar:SetStatusBarTexture(SFrames:GetTexture()) + f.manaBar:SetStatusBarColor(0.30, 0.65, 1.0, 0.90) + f.manaBar:SetMinMaxValues(0, 100) + f.manaBar:SetValue(0) + f.manaBar:SetFrameLevel(f:GetFrameLevel() + 1) + f.manaBar.bg = f.manaBar:CreateTexture(nil, "BACKGROUND") + f.manaBar.bg:SetAllPoints() + f.manaBar.bg:SetTexture(SFrames:GetTexture()) + f.manaBar.bg:SetVertexColor(0.05, 0.10, 0.20, 0.7) + f.manaBar:Hide() + + -- Outline/shadow setup for text to make it pop + f.nameText:SetShadowColor(0, 0, 0, 1) + f.nameText:SetShadowOffset(1, -1) + f.healthText:SetShadowColor(0, 0, 0, 1) + f.healthText:SetShadowOffset(1, -1) + f.powerText:SetShadowColor(0, 0, 0, 1) + f.powerText:SetShadowOffset(1, -1) + f.manaText:SetShadowColor(0, 0, 0, 1) + f.manaText:SetShadowOffset(1, -1) + + -- Resting Indicator (animated zzz on portrait) + local restOverlay = CreateFrame("Frame", nil, f) + restOverlay:SetFrameLevel((f:GetFrameLevel() or 0) + 6) + restOverlay:SetWidth(pWidth) + restOverlay:SetHeight(SFrames.Config.height) + restOverlay:SetPoint("CENTER", f.portrait, "CENTER", 0, 0) + f.restOverlay = restOverlay + + local zLetters = {} + for i = 1, 3 do + local zf = CreateFrame("Frame", nil, restOverlay) + zf:SetWidth(16) + zf:SetHeight(16) + local zt = zf:CreateFontString(nil, "OVERLAY") + zt:SetFont(SFrames:GetFont(), 8 + (i - 1) * 3, "OUTLINE") + zt:SetText("z") + zt:SetTextColor(0.85, 0.85, 1.0) + zt:SetShadowColor(0, 0, 0, 0.8) + zt:SetShadowOffset(1, -1) + zt:SetAllPoints(zf) + zf.text = zt + zf.phase = (i - 1) * 1.2 + zf.baseX = (i - 1) * 6 - 2 + zf.baseY = (i - 1) * 5 + zf:SetPoint("BOTTOMLEFT", restOverlay, "CENTER", zf.baseX, zf.baseY - 4) + zLetters[i] = zf + end + f.zLetters = zLetters + + local restElapsed = 0 + restOverlay:SetScript("OnUpdate", function() + restElapsed = restElapsed + arg1 + for idx = 1, 3 do + local zf = zLetters[idx] + local t = math.mod(restElapsed + zf.phase, 3.6) + local ratio = t / 3.6 + local floatY = ratio * 14 + local alpha + if ratio < 0.15 then + alpha = ratio / 0.15 + elseif ratio < 0.7 then + alpha = 1.0 + else + alpha = 1.0 - (ratio - 0.7) / 0.3 + end + if alpha < 0 then alpha = 0 end + if alpha > 1 then alpha = 1 end + zf.text:SetAlpha(alpha) + zf:ClearAllPoints() + zf:SetPoint("BOTTOMLEFT", restOverlay, "CENTER", zf.baseX, zf.baseY - 4 + floatY) + end + end) + restOverlay:Hide() + + -- Class Icon Badge (overlaid on portrait, top-right corner with 1/3 outside) + f.classIcon = SFrames:CreateClassIcon(f, 16) + f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0) + + -- Party Leader Icon + local leaderOvr = CreateFrame("Frame", nil, f) + leaderOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 4) + leaderOvr:SetAllPoints(f) + f.leaderIcon = leaderOvr:CreateTexture(nil, "OVERLAY") + f.leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-LeaderIcon") + f.leaderIcon:SetWidth(16) + f.leaderIcon:SetHeight(16) + f.leaderIcon:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -4, 4) + f.leaderIcon:Hide() + + -- Raid Target Icon (top center of health bar, half outside frame) + local raidIconSize = 22 + local raidIconOvr = CreateFrame("Frame", nil, f) + raidIconOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 5) + raidIconOvr:SetWidth(raidIconSize) + raidIconOvr:SetHeight(raidIconSize) + raidIconOvr:SetPoint("CENTER", f.health, "TOP", 0, 0) + f.raidIcon = raidIconOvr:CreateTexture(nil, "OVERLAY") + f.raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons") + f.raidIcon:SetAllPoints(raidIconOvr) + f.raidIcon:Hide() + f.raidIconOverlay = raidIconOvr + + self.frame = f + self:ApplyConfig() + self.frame:Show() -- Ensure it's explicitly shown + self:UpdateAll() + + -- Events + SFrames:RegisterEvent("UNIT_HEALTH", function() if arg1 == "player" then self:UpdateHealth() end end) + SFrames:RegisterEvent("UNIT_MAXHEALTH", function() if arg1 == "player" then self:UpdateHealth() end end) + SFrames:RegisterEvent("UNIT_MANA", function() if arg1 == "player" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_MAXMANA", function() if arg1 == "player" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_ENERGY", function() if arg1 == "player" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_MAXENERGY", function() if arg1 == "player" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_RAGE", function() if arg1 == "player" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_MAXRAGE", function() if arg1 == "player" then self:UpdatePower() end end) + SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:UpdateAll() end) + SFrames:RegisterEvent("PLAYER_LEVEL_UP", function() + if arg1 then self.currentLevel = arg1 end + self:UpdateAll() + if arg1 and mod(arg1, 2) == 0 and (not SFramesDB or SFramesDB.trainerReminder ~= false) then + self:ShowTrainerReminder(arg1) + end + end) + SFrames:RegisterEvent("PARTY_MEMBERS_CHANGED", function() self:UpdateLeaderIcon() end) + SFrames:RegisterEvent("PARTY_LEADER_CHANGED", function() self:UpdateLeaderIcon() end) + SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcon() end) + SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function() if arg1 == "player" then self.frame.portrait:SetUnit("player") self.frame.portrait:SetCamera(0) self.frame.portrait:SetPosition(-1.0, 0, 0) end end) + SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if arg1 == "player" then self:UpdatePowerType(); self:UpdatePower() end end) + SFrames:RegisterEvent("UPDATE_SHAPESHIFT_FORM", function() self:UpdatePowerType(); self:UpdatePower() end) + SFrames:RegisterEvent("PLAYER_UPDATE_RESTING", function() self:UpdateRestingStatus() end) + + f.unit = "player" + f:SetScript("OnEnter", function() + GameTooltip_SetDefaultAnchor(GameTooltip, this) + GameTooltip:SetUnit(this.unit) + GameTooltip:Show() + end) + f:SetScript("OnLeave", function() + GameTooltip:Hide() + end) +end + +function SFrames.Player:HasSpellInBook(spellName) + local i = 1 + while true do + local name = GetSpellName(i, BOOKTYPE_SPELL) + if not name then return false end + if name == spellName then return true end + i = i + 1 + end +end + +function SFrames.Player:GetSpellIcon(skillDisplayName) + local baseName = string.gsub(skillDisplayName, " %d+级$", "") + local altName1 = string.gsub(baseName, ":", ":") + local altName2 = string.gsub(baseName, ":", ":") + local i = 1 + while true do + local name = GetSpellName(i, BOOKTYPE_SPELL) + if not name then break end + if name == baseName or name == altName1 or name == altName2 then + return GetSpellTexture(i, BOOKTYPE_SPELL) + end + i = i + 1 + end + return nil +end + +function SFrames.Player:ShowTrainerReminder(newLevel) + local _, classEn = UnitClass("player") + local classNames = { + WARRIOR = "战士", PALADIN = "圣骑士", HUNTER = "猎人", + ROGUE = "盗贼", PRIEST = "牧师", SHAMAN = "萨满祭司", + MAGE = "法师", WARLOCK = "术士", DRUID = "德鲁伊", + } + local className = classNames[classEn] or UnitClass("player") + + local classFallbackIcons = { + WARRIOR = "Interface\\Icons\\Ability_Warrior_OffensiveStance", + PALADIN = "Interface\\Icons\\Spell_Holy_HolyBolt", + HUNTER = "Interface\\Icons\\Ability_Marksmanship", + ROGUE = "Interface\\Icons\\Ability_BackStab", + PRIEST = "Interface\\Icons\\Spell_Holy_HolyBolt", + SHAMAN = "Interface\\Icons\\Spell_Nature_Lightning", + MAGE = "Interface\\Icons\\Spell_Frost_IceStorm", + WARLOCK = "Interface\\Icons\\Spell_Shadow_DeathCoil", + DRUID = "Interface\\Icons\\Spell_Nature_Regeneration", + } + local fallbackIcon = classFallbackIcons[classEn] or "Interface\\Icons\\Trade_Engraving" + + local allSkills = {} + local allIcons = {} + + local baseSkills = SFrames.ClassSkillData and SFrames.ClassSkillData[classEn] and SFrames.ClassSkillData[classEn][newLevel] + if baseSkills then + for _, s in ipairs(baseSkills) do + table.insert(allSkills, s) + table.insert(allIcons, self:GetSpellIcon(s) or fallbackIcon) + end + end + + local talentData = SFrames.TalentTrainerSkills and SFrames.TalentTrainerSkills[classEn] and SFrames.TalentTrainerSkills[classEn][newLevel] + local talentSkills = {} + if talentData then + for _, entry in ipairs(talentData) do + local displayName = entry[1] + local requiredSpell = entry[2] + if self:HasSpellInBook(requiredSpell) then + table.insert(allSkills, displayName) + table.insert(allIcons, self:GetSpellIcon(displayName) or fallbackIcon) + table.insert(talentSkills, displayName) + end + end + end + + local mountQuest = SFrames.ClassMountQuests and SFrames.ClassMountQuests[classEn] and SFrames.ClassMountQuests[classEn][newLevel] + + local skillCount = table.getn(allSkills) + local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9" + + SFrames:Print(string.format("已达到 %d 级!你的%s训练师有新技能可以学习。", newLevel, className)) + if skillCount > 0 then + DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r 可学习的技能(共 " .. skillCount .. " 项):") + local line = " " + local lineCount = 0 + for i = 1, skillCount do + if lineCount > 0 then line = line .. ", " end + line = line .. "|cffffd100" .. allSkills[i] .. "|r" + lineCount = lineCount + 1 + if i == skillCount or lineCount == 4 then + DEFAULT_CHAT_FRAME:AddMessage(line) + line = " " + lineCount = 0 + end + end + end + if table.getn(talentSkills) > 0 then + DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cff00ff00(天赋)|r " .. table.concat(talentSkills, ", ")) + end + if mountQuest then + DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffffff00★|r " .. mountQuest) + end + + local bannerMsg = string.format("Lv.%d - %s训练师有 %d 项新技能可学习", newLevel, className, skillCount) + if mountQuest then + bannerMsg = bannerMsg .. " + " .. mountQuest + end + if skillCount == 0 and not mountQuest then + bannerMsg = string.format("Lv.%d - 前往%s训练师查看可学技能", newLevel, className) + elseif skillCount == 0 and mountQuest then + bannerMsg = string.format("Lv.%d - %s", newLevel, mountQuest) + end + UIErrorsFrame:AddMessage(bannerMsg, 1.0, 0.82, 0.0, 1, 5) + PlaySound("LEVELUP") + + if not self.trainerReminderFrame then + local fr = CreateFrame("Frame", "NanamiTrainerReminder", UIParent) + fr:SetWidth(440) + fr:SetHeight(106) + fr:SetPoint("TOP", UIParent, "TOP", 0, -120) + fr:SetFrameStrata("DIALOG") + + local bg = fr:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints(fr) + bg:SetTexture(0, 0, 0, 0.78) + + local border = fr:CreateTexture(nil, "BORDER") + border:SetPoint("TOPLEFT", fr, "TOPLEFT", -1, 1) + border:SetPoint("BOTTOMRIGHT", fr, "BOTTOMRIGHT", 1, -1) + border:SetTexture(1, 0.82, 0, 0.3) + + local icon = fr:CreateTexture(nil, "ARTWORK") + icon:SetWidth(36) + icon:SetHeight(36) + icon:SetPoint("TOPLEFT", fr, "TOPLEFT", 10, -6) + icon:SetTexture("Interface\\Icons\\INV_Misc_Book_11") + fr.icon = icon + + local title = fr:CreateFontString(nil, "OVERLAY", "GameFontNormal") + title:SetPoint("TOPLEFT", icon, "TOPRIGHT", 10, -2) + title:SetPoint("RIGHT", fr, "RIGHT", -10, 0) + title:SetJustifyH("LEFT") + title:SetTextColor(1, 0.82, 0) + fr.title = title + + local subtitle = fr:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + subtitle:SetPoint("TOPLEFT", icon, "TOPRIGHT", 10, -18) + subtitle:SetPoint("RIGHT", fr, "RIGHT", -10, 0) + subtitle:SetJustifyH("LEFT") + subtitle:SetTextColor(0.75, 0.75, 0.75) + fr.subtitle = subtitle + + fr.skillIcons = {} + fr.skillBorders = {} + local maxIcons = 13 + for idx = 1, maxIcons do + local bdr = fr:CreateTexture(nil, "BORDER") + bdr:SetWidth(30) + bdr:SetHeight(30) + bdr:SetPoint("TOPLEFT", fr, "TOPLEFT", 9 + (idx - 1) * 32, -45) + bdr:SetTexture(1, 0.82, 0, 0.25) + bdr:Hide() + fr.skillBorders[idx] = bdr + + local si = fr:CreateTexture(nil, "ARTWORK") + si:SetWidth(28) + si:SetHeight(28) + si:SetPoint("CENTER", bdr, "CENTER", 0, 0) + si:SetTexCoord(0.07, 0.93, 0.07, 0.93) + si:Hide() + fr.skillIcons[idx] = si + end + + local detail = fr:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + detail:SetPoint("BOTTOMLEFT", fr, "BOTTOMLEFT", 10, 8) + detail:SetPoint("RIGHT", fr, "RIGHT", -10, 0) + detail:SetJustifyH("LEFT") + detail:SetTextColor(0.85, 0.85, 0.85) + fr.detail = detail + + fr:SetAlpha(0) + fr:Hide() + self.trainerReminderFrame = fr + end + + local fr = self.trainerReminderFrame + + for idx = 1, 13 do + fr.skillIcons[idx]:Hide() + fr.skillBorders[idx]:Hide() + end + + local iconCount = 0 + for i = 1, skillCount do + if iconCount >= 13 then break end + iconCount = iconCount + 1 + fr.skillIcons[iconCount]:SetTexture(allIcons[i]) + fr.skillIcons[iconCount]:Show() + fr.skillBorders[iconCount]:Show() + end + if mountQuest and iconCount < 13 then + iconCount = iconCount + 1 + fr.skillIcons[iconCount]:SetTexture("Interface\\Icons\\Spell_Nature_Swiftness") + fr.skillIcons[iconCount]:Show() + fr.skillBorders[iconCount]:Show() + end + + if iconCount > 0 then + fr:SetHeight(106) + else + fr:SetHeight(72) + end + + fr.title:SetText(string.format("已达到 |cffffffff%d|r 级 — %s训练师有新技能", newLevel, className)) + if skillCount > 0 or mountQuest then + local countText = "" + if skillCount > 0 then + countText = skillCount .. " 项技能" + end + if mountQuest then + if countText ~= "" then countText = countText .. " + " end + countText = countText .. mountQuest + end + fr.subtitle:SetText(countText) + fr.detail:SetText("详见聊天窗口") + else + fr.subtitle:SetText("") + fr.detail:SetText("前往职业训练师查看可学习的技能") + end + fr:Show() + fr:SetAlpha(0) + + fr.fadeState = "in" + fr.fadeTimer = 0 + fr.holdTimer = 0 + fr:SetScript("OnUpdate", function() + local dt = arg1 + if fr.fadeState == "in" then + fr.fadeTimer = fr.fadeTimer + dt + local a = fr.fadeTimer / 0.5 + if a >= 1 then + a = 1 + fr.fadeState = "hold" + end + fr:SetAlpha(a) + elseif fr.fadeState == "hold" then + fr.holdTimer = fr.holdTimer + dt + if fr.holdTimer >= 8 then + fr.fadeState = "out" + fr.fadeTimer = 0 + end + elseif fr.fadeState == "out" then + fr.fadeTimer = fr.fadeTimer + dt + local a = 1 - fr.fadeTimer / 1.0 + if a <= 0 then + a = 0 + fr:Hide() + fr:SetScript("OnUpdate", nil) + end + fr:SetAlpha(a) + end + end) +end + +function SFrames.Player:UpdateAll() + if not self.frame then return end + self:UpdateHealth() + self:UpdatePowerType() + self:UpdatePower() + self:UpdateLeaderIcon() + self:UpdateRaidIcon() + self:UpdateRestingStatus() + + local name = UnitName("player") or "" + -- Use the stored level from PLAYER_LEVEL_UP if it exists, since API might lag slightly + local level = self.currentLevel or UnitLevel("player") + local formattedLevel = string.format("|cffffff00%d|r", level) + + if SFramesDB and SFramesDB.showLevel == false then + formattedLevel = "" + else + formattedLevel = formattedLevel .. " " + end + + self.frame.portrait:SetUnit("player") + self.frame.portrait:SetCamera(0) + self.frame.portrait:SetPosition(-1.0, 0, 0) + + -- Class Color for Health + local localizedClass, class = UnitClass("player") + local className = GetChineseClassName(class, localizedClass) + local nameLine = formattedLevel .. name + local showClassText = not (SFramesDB and SFramesDB.playerShowClass == false) + if showClassText and className and className ~= "" then + nameLine = nameLine .. " " .. className + end + + if not (SFramesDB and SFramesDB.playerShowClassIcon == false) then + SFrames:SetClassIcon(self.frame.classIcon, class) + else + self.frame.classIcon:Hide() + if self.frame.classIcon.overlay then self.frame.classIcon.overlay:Hide() end + end + + local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false) + + if useClassColor and class and SFrames.Config.colors.class[class] then + local color = SFrames.Config.colors.class[class] + self.frame.health:SetStatusBarColor(color.r, color.g, color.b) + + -- Apply Class Color to Name + self.frame.nameText:SetText(nameLine) + self.frame.nameText:SetTextColor(color.r, color.g, color.b) + else + self.frame.health:SetStatusBarColor(0, 1, 0) + self.frame.nameText:SetText(nameLine) + self.frame.nameText:SetTextColor(1, 1, 1) + end +end + +function SFrames.Player:UpdateRestingStatus() + if not self.frame or not self.frame.restOverlay then return end + if IsResting() then + self.frame.restOverlay:Show() + else + self.frame.restOverlay:Hide() + end +end + +function SFrames.Player:UpdateLeaderIcon() + if IsPartyLeader() then + self.frame.leaderIcon:Show() + else + self.frame.leaderIcon:Hide() + end +end + +function SFrames.Player:UpdateRaidIcon() + if not (self.frame and self.frame.raidIcon) then return end + if not GetRaidTargetIndex then + self.frame.raidIcon:Hide() + return + end + local index = GetRaidTargetIndex("player") + if index and index > 0 and index <= 8 then + local col = math.mod(index - 1, 4) + local row = math.floor((index - 1) / 4) + self.frame.raidIcon:SetTexCoord(col * 0.25, (col + 1) * 0.25, row * 0.25, (row + 1) * 0.25) + self.frame.raidIcon:Show() + else + self.frame.raidIcon:Hide() + end +end + +function SFrames.Player:UpdateHealth() + local hp = UnitHealth("player") + local maxHp = UnitHealthMax("player") + self.frame.health:SetMinMaxValues(0, maxHp) + self.frame.health:SetValue(hp) + + if maxHp > 0 then + self.frame.healthText:SetText(hp .. " / " .. maxHp) + else + self.frame.healthText:SetText(hp) + end + + self:UpdateHealPrediction() +end + +function SFrames.Player:UpdateHealPrediction() + if not (self.frame and self.frame.health and self.frame.health.healPredMine and self.frame.health.healPredOther) then return end + local predMine = self.frame.health.healPredMine + local predOther = self.frame.health.healPredOther + + local function HidePredictions() + predMine:Hide() + predOther:Hide() + end + + local hp = UnitHealth("player") or 0 + local maxHp = UnitHealthMax("player") or 0 + if maxHp <= 0 or hp >= maxHp or UnitIsDeadOrGhost("player") then + HidePredictions() + return + end + + local _, mineIncoming, othersIncoming = GetIncomingHeals("player") + local missing = maxHp - hp + if missing <= 0 then + HidePredictions() + return + end + + local mineShown = math.min(math.max(0, mineIncoming), missing) + local remaining = missing - mineShown + local otherShown = math.min(math.max(0, othersIncoming), remaining) + if mineShown <= 0 and otherShown <= 0 then + HidePredictions() + return + end + + local barWidth = self.frame.health:GetWidth() or 0 + if barWidth <= 0 then + HidePredictions() + return + end + + local currentWidth = math.floor((hp / maxHp) * barWidth + 0.5) + if currentWidth < 0 then currentWidth = 0 end + if currentWidth > barWidth then currentWidth = barWidth end + + local availableWidth = barWidth - currentWidth + if availableWidth <= 0 then + HidePredictions() + return + end + + local mineWidth = math.floor((mineShown / maxHp) * barWidth + 0.5) + local otherWidth = math.floor((otherShown / maxHp) * barWidth + 0.5) + if mineWidth < 0 then mineWidth = 0 end + if otherWidth < 0 then otherWidth = 0 end + if mineWidth > availableWidth then mineWidth = availableWidth end + if otherWidth > (availableWidth - mineWidth) then + otherWidth = availableWidth - mineWidth + end + + if mineWidth > 0 then + predMine:ClearAllPoints() + predMine:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth, 0) + predMine:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth, 0) + predMine:SetWidth(mineWidth) + predMine:Show() + else + predMine:Hide() + end + + if otherWidth > 0 then + predOther:ClearAllPoints() + predOther:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth + mineWidth, 0) + predOther:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth + mineWidth, 0) + predOther:SetWidth(otherWidth) + predOther:Show() + else + predOther:Hide() + end +end + +function SFrames.Player:UpdatePowerType() + local powerType = UnitPowerType("player") + local color = SFrames.Config.colors.power[powerType] + if color then + self.frame.power:SetStatusBarColor(color.r, color.g, color.b) + else + self.frame.power:SetStatusBarColor(0, 0, 1) + end +end + +function SFrames.Player:GetDruidAltMana(currentPower, currentMaxPower) + -- Method 1: DruidManaLib (tracks regen ticks, MP5, talents, shapeshift cost) + if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("DruidManaLib-1.0") then + local ok, lib = pcall(function() return AceLibrary("DruidManaLib-1.0") end) + if ok and lib and lib.GetMana then + local mana, maxMana = lib:GetMana() + if type(mana) == "number" and type(maxMana) == "number" and maxMana > 0 then + return math.floor(mana + 0.5), math.floor(maxMana + 0.5) + end + end + end + + -- Method 2: SuperWow returns real mana as second value of UnitMana + if CheckSuperWow then + local ok, hasSW = pcall(CheckSuperWow) + if ok and hasSW then + local ok2, _, realMana = pcall(UnitMana, "player") + local ok3, _, realMax = pcall(UnitManaMax, "player") + if ok2 and ok3 and type(realMana) == "number" and type(realMax) == "number" and realMax > 0 then + return realMana, realMax + end + end + end + + -- Method 3: TBC-style UnitPower API + if UnitPower and UnitPowerMax then + local okMana, pMana = pcall(function() return UnitPower("player", 0) end) + local okMax, pMax = pcall(function() return UnitPowerMax("player", 0) end) + if okMana and okMax and type(pMana) == "number" and type(pMax) == "number" and pMax > 0 then + if not (pMax == currentMaxPower and pMana == currentPower) and pMax > 100 then + return pMana, pMax + end + end + end + + return nil, nil +end + +function SFrames.Player:UpdateFiveSecondRule() + if not (self.frame and self.frame.power and self.frame.power.fsrGlow) then return end + + local powerBar = self.frame.power + local glow = powerBar.fsrGlow + + local function HideTicker() + glow:Hide() + end + + local powerType = UnitPowerType("player") + if powerType ~= 0 then + self.fiveSecondStart = nil + self.fiveSecondPendingStart = nil + self.fiveSecondLastMana = nil + HideTicker() + return + end + + local now = GetTime() + + -- Continuous monitoring: whenever mana drops, (re)start 5-second rule. + -- If mana drops while hard-casting, delay start until cast end. + local currentMana = UnitMana("player") or 0 + if self.fiveSecondLastMana and currentMana < self.fiveSecondLastMana then + local delay = 0 + local cb = self.frame and self.frame.castbar + if cb and cb.casting and cb.startTime and cb.maxValue then + local castEnd = cb.startTime + cb.maxValue + if castEnd > now then + delay = castEnd - now + end + end + + local startAt = now + delay + 0.08 + if delay > 0 then + self.fiveSecondPendingStart = startAt + else + self.fiveSecondStart = startAt + self.fiveSecondPendingStart = nil + end + end + self.fiveSecondLastMana = currentMana + + if self.fiveSecondPendingStart then + if now >= self.fiveSecondPendingStart then + self.fiveSecondStart = self.fiveSecondPendingStart + self.fiveSecondPendingStart = nil + else + HideTicker() + return + end + end + + local startTime = self.fiveSecondStart + if not startTime then + HideTicker() + return + end + + local elapsed = now - startTime + if elapsed < 0 then + HideTicker() + return + end + if elapsed >= 5 then + self.fiveSecondStart = nil + HideTicker() + return + end + + local barWidth = powerBar:GetWidth() or 0 + if barWidth <= 0 then + HideTicker() + return + end + + local progress = elapsed / 5 + if progress < 0 then progress = 0 end + if progress > 1 then progress = 1 end + + local glowWidth = 40 + if glowWidth > barWidth then glowWidth = barWidth end + + local maxGlowX = barWidth - glowWidth + + -- Move strictly from left edge to right edge across full 5 seconds. + local glowX = math.floor(progress * maxGlowX + 0.5) + + local pulse = 0.82 + 0.18 * math.sin(GetTime() * 10) + + glow:ClearAllPoints() + glow:SetPoint("TOPLEFT", powerBar, "TOPLEFT", glowX, 0) + glow:SetPoint("BOTTOMLEFT", powerBar, "BOTTOMLEFT", glowX, 0) + glow:SetWidth(glowWidth) + glow:SetAlpha(0.62 * pulse) + glow:Show() +end + +function SFrames.Player:UpdatePower() + local power = UnitMana("player") + local maxPower = UnitManaMax("player") + self.frame.power:SetMinMaxValues(0, maxPower) + self.frame.power:SetValue(power) + self.frame.powerText:SetText(power .. " / " .. maxPower) + + local _, class = UnitClass("player") + local powerType = UnitPowerType("player") + + self:UpdateFiveSecondRule() + + if class ~= "DRUID" then + if self.frame.manaText then self.frame.manaText:Hide() end + if self.frame.manaBar then self.frame.manaBar:Hide() end + return + end + + if not self.druidManaCache then + self.druidManaCache = { value = 0, max = 0, ts = GetTime() } + end + + if powerType == 0 then + self.druidManaCache.value = power or 0 + self.druidManaCache.max = maxPower or 0 + self.druidManaCache.ts = GetTime() + if self.frame.manaText then self.frame.manaText:Hide() end + if self.frame.manaBar then self.frame.manaBar:Hide() end + return + end + + if not self.frame.manaText then + return + end + + local mana, maxMana = self:GetDruidAltMana(power, maxPower) + if mana and maxMana and maxMana > 0 then + self.druidManaCache.value = mana + self.druidManaCache.max = maxMana + self.druidManaCache.ts = GetTime() + else + local cache = self.druidManaCache + if cache and cache.max and cache.max > 0 then + local now = GetTime() + local dt = now - (cache.ts or now) + if dt < 0 then dt = 0 end + + local regen = 0 + if GetManaRegen then + local base, casting = GetManaRegen() + if type(base) == "number" and base > 0 then + regen = base + elseif type(casting) == "number" and casting > 0 then + regen = casting + end + end + if (not regen or regen <= 0) and UnitStat then + local ok, _, spi = pcall(UnitStat, "player", 5) + if not (ok and type(spi) == "number" and spi > 0) then + ok, spi = pcall(function() return UnitStat("player", 5) end) + end + if ok and type(spi) == "number" and spi > 0 then + regen = (math.ceil(spi / 5) + 15) / 2 + end + end + if regen and regen > 0 and dt > 0 then + cache.value = math.min(cache.max, (cache.value or 0) + regen * dt) + end + cache.ts = now + mana = math.floor((cache.value or 0) + 0.5) + maxMana = cache.max + end + end + + if maxMana and maxMana > 0 then + local pct = math.floor(mana / maxMana * 100 + 0.5) + self.frame.manaText:SetText(pct .. "% " .. mana) + self.frame.manaText:Show() + if self.frame.manaBar then + self.frame.manaBar:SetMinMaxValues(0, maxMana) + self.frame.manaBar:SetValue(mana) + self.frame.manaBar:Show() + end + else + self.frame.manaText:SetText("--") + self.frame.manaText:Show() + if self.frame.manaBar then self.frame.manaBar:Hide() end + end +end + +-------------------------------------------------------------------------------- +-- Player Auras (Buffs / Debuffs) +-------------------------------------------------------------------------------- + +function SFrames.Player:CreateAuras() + -- Create 16 Buff Slots + self.frame.buffs = {} + self.frame.debuffs = {} + local size = 24 + local spacing = 2 + local rowSpacing = 1 + local buffsPerRow = 9 + + for i = 1, 16 do + local b = CreateFrame("Button", "SFramesPlayerBuff"..i, self.frame) + b:SetWidth(size) + b:SetHeight(size) + SFrames:CreateUnitBackdrop(b) + + b.icon = b:CreateTexture(nil, "ARTWORK") + b.icon:SetPoint("TOPLEFT", b, "TOPLEFT", 1, -1) + b.icon:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -1, 1) + b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + + b.cdText = SFrames:CreateFontString(b, 9, "CENTER") + b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1) + b.cdText:SetTextColor(1, 0.82, 0) + b.cdText:SetShadowColor(0, 0, 0, 1) + b.cdText:SetShadowOffset(1, -1) + + -- Tooltip support for precise buff checking + b:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") + GameTooltip:SetPlayerBuff(this.buffIndex) + end) + b:SetScript("OnLeave", function() GameTooltip:Hide() end) + + -- Default row anchor + if i == 1 then + b:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", 0, -1) + elseif math.mod(i - 1, buffsPerRow) == 0 then + b:SetPoint("TOP", self.frame.buffs[i-buffsPerRow], "BOTTOM", 0, -rowSpacing) + else + b:SetPoint("LEFT", self.frame.buffs[i-1], "RIGHT", spacing, 0) + end + + b:Hide() + self.frame.buffs[i] = b + end +end + +function SFrames.Player:UpdateAuras() + local timeNow = GetTime() + for i = 1, 16 do + local b = self.frame.buffs[i] + local buffIndex, untilCancelled = GetPlayerBuff(i - 1, "HELPFUL") + if buffIndex >= 0 then + local texture = GetPlayerBuffTexture(buffIndex) + if texture then + b.icon:SetTexture(texture) + b.buffIndex = buffIndex + b:Show() + + local timeLeft = GetPlayerBuffTimeLeft(buffIndex) + if timeLeft and timeLeft > 0 and timeLeft < 9999 then + b.cdText:SetText(SFrames:FormatTime(timeLeft)) + else + b.cdText:SetText("") + end + else + b:Hide() + end + else + b:Hide() + end + end +end + +-- Initialization Hook for Auras and Castbar +local origInit = SFrames.Player.Initialize +function SFrames.Player:Initialize() + origInit(self) + + -- Setup Auras + self:CreateAuras() + self.auraUpdater = CreateFrame("Frame") + self.auraUpdater.timer = 0 + self.auraUpdater:SetScript("OnUpdate", function() + this.timer = this.timer + arg1 + SFrames.Player:UpdateFiveSecondRule() + if this.timer >= 0.2 then + SFrames.Player:UpdateAuras() + SFrames.Player:UpdatePower() + SFrames.Player:UpdateHealPrediction() + this.timer = 0 + end + end) + + -- Setup Castbar + self:CreateCastbar() + + -- Hide default castbar + if CastingBarFrame then + CastingBarFrame:UnregisterAllEvents() + CastingBarFrame:Hide() + end +end + +-------------------------------------------------------------------------------- +-- Player Castbar +-------------------------------------------------------------------------------- + +function SFrames.Player:CreateCastbar() + local cb = SFrames:CreateStatusBar(self.frame, "SFramesPlayerCastbar") + cb:SetHeight(SFrames.Config.castbarHeight) + cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", 0, 6) + cb:SetPoint("BOTTOMLEFT", self.frame.portrait, "TOPLEFT", SFrames.Config.castbarHeight + 6, 6) + + local cbbg = CreateFrame("Frame", nil, self.frame) + cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1) + cbbg:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 1, -1) + cbbg:SetFrameLevel(cb:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(cbbg) + + cb.bg = cb:CreateTexture(nil, "BACKGROUND") + cb.bg:SetAllPoints() + cb.bg:SetTexture(SFrames:GetTexture()) + cb.bg:SetVertexColor(0.2, 0.2, 0.2, 1) + cb:SetStatusBarColor(1, 0.7, 0) + + cb.text = SFrames:CreateFontString(cb, 10, "LEFT") + cb.text:SetPoint("LEFT", cb, "LEFT", 4, 0) + + cb.time = SFrames:CreateFontString(cb, 10, "RIGHT") + cb.time:SetPoint("RIGHT", cb, "RIGHT", -4, 0) + + cb.icon = cb:CreateTexture(nil, "ARTWORK") + cb.icon:SetWidth(SFrames.Config.castbarHeight + 2) + cb.icon:SetHeight(SFrames.Config.castbarHeight + 2) + cb.icon:SetPoint("RIGHT", cb, "LEFT", -4, 0) + cb.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + + -- Icon Backdrop + local ibg = CreateFrame("Frame", nil, self.frame) + ibg:SetPoint("TOPLEFT", cb.icon, "TOPLEFT", -1, 1) + ibg:SetPoint("BOTTOMRIGHT", cb.icon, "BOTTOMRIGHT", 1, -1) + ibg:SetFrameLevel(cb:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(ibg) + + cb:Hide() + cbbg:Hide() + cb.icon:Hide() + ibg:Hide() + + self.frame.castbar = cb + self.frame.castbar.cbbg = cbbg + self.frame.castbar.ibg = ibg + + cb:SetScript("OnUpdate", function() SFrames.Player:CastbarOnUpdate() end) + + -- Hook events + SFrames:RegisterEvent("SPELLCAST_START", function() self:CastbarStart(arg1, arg2) end) + SFrames:RegisterEvent("SPELLCAST_STOP", function() self:CastbarStop() end) + SFrames:RegisterEvent("SPELLCAST_FAILED", function() self:CastbarStop() end) + SFrames:RegisterEvent("SPELLCAST_INTERRUPTED", function() self:CastbarStop() end) + SFrames:RegisterEvent("SPELLCAST_DELAYED", function() self:CastbarDelayed(arg1) end) + SFrames:RegisterEvent("SPELLCAST_CHANNEL_START", function() self:CastbarChannelStart(arg1, arg2) end) + SFrames:RegisterEvent("SPELLCAST_CHANNEL_UPDATE", function() self:CastbarChannelUpdate(arg1) end) + SFrames:RegisterEvent("SPELLCAST_CHANNEL_STOP", function() self:CastbarStop() end) +end + +function SFrames.Player:CastbarStart(spellName, duration) + local cb = self.frame.castbar + cb.casting = true + cb.channeling = nil + cb.fadeOut = nil + cb.startTime = GetTime() + cb.maxValue = duration / 1000 + cb:SetMinMaxValues(0, cb.maxValue) + cb:SetValue(0) + cb.text:SetText(spellName) + + local texture + local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo) + if _UnitCastingInfo then + local _, _, _, tex = _UnitCastingInfo("player") + texture = tex + end + if texture then + cb.icon:SetTexture(texture) + cb.icon:Show() + cb.ibg:Show() + else + cb.icon:Hide() + cb.ibg:Hide() + end + + cb:SetAlpha(1) + cb.cbbg:SetAlpha(1) + if texture then + cb.icon:SetAlpha(1) + cb.ibg:SetAlpha(1) + end + cb:Show() + cb.cbbg:Show() +end + +function SFrames.Player:CastbarChannelStart(duration, spellName) + local cb = self.frame.castbar + cb.casting = nil + cb.channeling = true + cb.fadeOut = nil + cb.startTime = GetTime() + cb.maxValue = duration / 1000 + cb.endTime = cb.startTime + cb.maxValue + cb:SetMinMaxValues(0, cb.maxValue) + cb:SetValue(cb.maxValue) + cb.text:SetText(spellName) + + local texture + local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo) + if _UnitChannelInfo then + local _, _, _, tex = _UnitChannelInfo("player") + texture = tex + end + if texture then + cb.icon:SetTexture(texture) + cb.icon:Show() + cb.ibg:Show() + else + cb.icon:Hide() + cb.ibg:Hide() + end + + cb:SetAlpha(1) + cb.cbbg:SetAlpha(1) + if texture then + cb.icon:SetAlpha(1) + cb.ibg:SetAlpha(1) + end + cb:Show() + cb.cbbg:Show() +end + +function SFrames.Player:CastbarStop() + local cb = self.frame.castbar + cb.casting = nil + cb.channeling = nil + cb.fadeOut = true + -- keep showing for a short fade out +end + +function SFrames.Player:CastbarDelayed(delay) + local cb = self.frame.castbar + if cb.casting then + cb.maxValue = cb.maxValue + (delay / 1000) + cb:SetMinMaxValues(0, cb.maxValue) + end +end + +function SFrames.Player:CastbarChannelUpdate(delay) + local cb = self.frame.castbar + if cb.channeling then + local add = delay / 1000 + cb.maxValue = cb.maxValue + add + cb.endTime = cb.endTime + add + cb:SetMinMaxValues(0, cb.maxValue) + end +end + +function SFrames.Player:CastbarOnUpdate() + local cb = self.frame.castbar + if cb.casting then + local elapsed = GetTime() - cb.startTime + if elapsed >= cb.maxValue then + cb.casting = nil + cb.fadeOut = true + cb:SetValue(cb.maxValue) + return + end + cb:SetValue(elapsed) + cb.time:SetText(string.format("%.1f", math.max(cb.maxValue - elapsed, 0))) + elseif cb.channeling then + local timeRemaining = cb.endTime - GetTime() + if timeRemaining <= 0 then + cb.channeling = nil + cb.fadeOut = true + cb:SetValue(0) + return + end + cb:SetValue(timeRemaining) + cb.time:SetText(string.format("%.1f", timeRemaining)) + elseif cb.fadeOut then + local alpha = cb:GetAlpha() - 0.05 + if alpha > 0 then + cb:SetAlpha(alpha) + cb.cbbg:SetAlpha(alpha) + cb.icon:SetAlpha(alpha) + cb.ibg:SetAlpha(alpha) + else + cb.fadeOut = nil + cb:Hide() + cb.cbbg:Hide() + cb.icon:Hide() + cb.ibg:Hide() + end + end +end diff --git a/Units/Raid.lua b/Units/Raid.lua new file mode 100644 index 0000000..042c169 --- /dev/null +++ b/Units/Raid.lua @@ -0,0 +1,1064 @@ +SFrames.Raid = {} +local _A = SFrames.ActiveTheme + +local RAID_FRAME_WIDTH = 60 +local RAID_FRAME_HEIGHT = 40 +local GROUP_PADDING = 8 +local UNIT_PADDING = 2 + +local RAID_UNIT_LOOKUP = {} +for i = 1, 40 do RAID_UNIT_LOOKUP["raid" .. i] = true end + +local function GetIncomingHeals(unit) + return SFrames:GetIncomingHeals(unit) +end + +function SFrames.Raid:GetMetrics() + local db = SFramesDB or {} + + local width = tonumber(db.raidFrameWidth) or RAID_FRAME_WIDTH + width = math.max(30, math.min(150, width)) + + local height = tonumber(db.raidFrameHeight) or RAID_FRAME_HEIGHT + height = math.max(20, math.min(80, height)) + + local healthHeight = tonumber(db.raidHealthHeight) or math.floor((height - 3) * 0.8) + healthHeight = math.max(10, math.min(height - 6, healthHeight)) + + local powerHeight = height - healthHeight - 3 + if not db.raidShowPower then + powerHeight = 0 + healthHeight = height - 2 + end + + local hgap = tonumber(db.raidHorizontalGap) or UNIT_PADDING + local vgap = tonumber(db.raidVerticalGap) or UNIT_PADDING + local groupGap = tonumber(db.raidGroupGap) or GROUP_PADDING + + local nameFont = tonumber(db.raidNameFontSize) or 10 + local valueFont = tonumber(db.raidValueFontSize) or 9 + + return { + width = width, + height = height, + healthHeight = healthHeight, + powerHeight = powerHeight, + horizontalGap = hgap, + verticalGap = vgap, + groupGap = groupGap, + nameFont = nameFont, + valueFont = valueFont, + showPower = db.raidShowPower ~= false, + } +end + +function SFrames.Raid:ApplyFrameStyle(frame, metrics) + if not frame then return end + + frame:SetWidth(metrics.width) + frame:SetHeight(metrics.height) + + if frame.health then + frame.health:ClearAllPoints() + frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1) + frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1) + frame.health:SetHeight(metrics.healthHeight) + end + + if frame.healthBGFrame then + frame.healthBGFrame:ClearAllPoints() + frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", -1, 1) + frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 1, -1) + end + + if frame.power then + if metrics.showPower then + frame.power:Show() + if frame.powerBGFrame then frame.powerBGFrame:Show() end + frame.power:ClearAllPoints() + frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1) + frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0) + frame.power:SetHeight(metrics.powerHeight) + else + frame.power:Hide() + if frame.powerBGFrame then frame.powerBGFrame:Hide() end + end + end + + local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" + local fontPath = SFrames:GetFont() + + if frame.nameText then + frame.nameText:SetFont(fontPath, metrics.nameFont, outline) + end + if frame.healthText then + frame.healthText:SetFont(fontPath, metrics.valueFont, outline) + end +end + +function SFrames.Raid:ApplyLayout() + if not (self.parent and self._framesBuilt) then return end + + local metrics = self:GetMetrics() + self.metrics = metrics + local mode = (SFramesDB and SFramesDB.raidLayout) or "horizontal" + local showLabel = SFramesDB and SFramesDB.raidShowGroupLabel ~= false + + local LABEL_H = 16 -- px above each column (horizontal mode) + local LABEL_W = 16 -- px left of each row (vertical mode) + + for g = 1, 8 do + local groupParent = self.groups[g] + groupParent:ClearAllPoints() + + if mode == "horizontal" then + local extraTop = showLabel and LABEL_H or 0 + if g == 1 then + groupParent:SetPoint("TOPLEFT", self.parent, "TOPLEFT", 0, 0) + else + groupParent:SetPoint("TOPLEFT", self.groups[g-1], "TOPRIGHT", metrics.groupGap, 0) + end + groupParent:SetWidth(metrics.width) + groupParent:SetHeight(extraTop + (metrics.height * 5) + (metrics.verticalGap * 4)) + else + local extraLeft = showLabel and LABEL_W or 0 + if g == 1 then + groupParent:SetPoint("TOPLEFT", self.parent, "TOPLEFT", 0, 0) + else + groupParent:SetPoint("TOPLEFT", self.groups[g-1], "BOTTOMLEFT", 0, -metrics.groupGap) + end + groupParent:SetWidth(extraLeft + (metrics.width * 5) + (metrics.horizontalGap * 4)) + groupParent:SetHeight(metrics.height) + end + + -- Position label; visibility is controlled per-group in UpdateAll + local lbl = self.groupLabels and self.groupLabels[g] + if lbl then + lbl:ClearAllPoints() + if showLabel then + if mode == "horizontal" then + -- Place label at the very top of the column, centered horizontally + -- Frames start at -LABEL_H, so the strip [0, -LABEL_H] is exclusively for the label + lbl:SetPoint("TOP", groupParent, "TOP", 0, -2) + else + -- Vertically centered on the left strip, centered horizontally within it + lbl:SetPoint("CENTER", groupParent, "LEFT", LABEL_W / 2, 0) + end + else + lbl:Hide() + end + end + + for i = 1, 5 do + local index = (g - 1) * 5 + i + local f = self.frames[index].frame + f:ClearAllPoints() + + if mode == "horizontal" then + local extraTop = showLabel and LABEL_H or 0 + if i == 1 then + -- Frames start below the label strip + f:SetPoint("TOPLEFT", groupParent, "TOPLEFT", 0, -extraTop) + else + f:SetPoint("TOPLEFT", self.frames[index-1].frame, "BOTTOMLEFT", 0, -metrics.verticalGap) + end + else + local extraLeft = showLabel and LABEL_W or 0 + if i == 1 then + f:SetPoint("TOPLEFT", groupParent, "TOPLEFT", extraLeft, 0) + else + f:SetPoint("TOPLEFT", self.frames[index-1].frame, "TOPRIGHT", metrics.horizontalGap, 0) + end + end + end + end + + local extraTop = showLabel and LABEL_H or 0 + local extraLeft = showLabel and LABEL_W or 0 + if mode == "horizontal" then + self.parent:SetWidth((metrics.width * 8) + (metrics.groupGap * 7)) + self.parent:SetHeight(extraTop + (metrics.height * 5) + (metrics.verticalGap * 4)) + else + self.parent:SetWidth(extraLeft + (metrics.width * 5) + (metrics.horizontalGap * 4)) + self.parent:SetHeight((metrics.height * 8) + (metrics.groupGap * 7)) + end +end + +function SFrames.Raid:SavePosition() + if not (self.parent and SFramesDB) then return end + if not SFramesDB.Positions then SFramesDB.Positions = {} end + local point, _, relativePoint, xOfs, yOfs = self.parent:GetPoint() + if point and relativePoint then + SFramesDB.Positions["RaidFrame"] = { + point = point, + relativePoint = relativePoint, + xOfs = xOfs or 0, + yOfs = yOfs or 0, + } + end +end + +function SFrames.Raid:ApplyPosition() + if not self.parent then return end + self.parent:ClearAllPoints() + local pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["RaidFrame"] + if pos and pos.point and pos.relativePoint and type(pos.xOfs) == "number" and type(pos.yOfs) == "number" then + self.parent:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs) + else + self.parent:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 15, -200) + end +end + +local function TryDropCursorOnUnit(unit) + if not unit or not UnitExists(unit) then return false end + if not CursorHasItem or not CursorHasItem() then return false end + if not DropItemOnUnit then return false end + + local ok = pcall(DropItemOnUnit, unit) + return not CursorHasItem() +end + +function SFrames.Raid:Initialize() + self.frames = {} + self.groups = {} + self._framesBuilt = false + + if not SFramesDB then SFramesDB = {} end + + local parent = CreateFrame("Frame", "SFramesRaidParent", UIParent) + local frameScale = (SFramesDB and type(SFramesDB.raidFrameScale) == "number") and SFramesDB.raidFrameScale or 1 + parent:SetScale(frameScale) + self.parent = parent + self:ApplyPosition() + + parent:SetMovable(true) + + self.groupLabels = {} + for g = 1, 8 do + local groupParent = CreateFrame("Frame", "SFramesRaidGroup"..g, parent) + self.groups[g] = groupParent + + local lbl = groupParent:CreateFontString(nil, "OVERLAY") + lbl:SetFont(SFrames:GetFont(), 9, "OUTLINE") + lbl:SetTextColor(0.85, 0.78, 0.92) + lbl:SetShadowColor(0, 0, 0, 1) + lbl:SetShadowOffset(1, -1) + lbl:SetText("小队 " .. g) + lbl:Hide() + self.groupLabels[g] = lbl + end + + SFrames:RegisterEvent("RAID_ROSTER_UPDATE", function() self:UpdateAll() end) + SFrames:RegisterEvent("PARTY_LEADER_CHANGED", function() self:UpdateAll() end) + SFrames:RegisterEvent("PLAYER_TARGET_CHANGED", function() self:UpdateTargetHighlight() end) + SFrames:RegisterEvent("UNIT_HEALTH", function() if RAID_UNIT_LOOKUP[arg1] then self:UpdateHealth(arg1) end end) + SFrames:RegisterEvent("UNIT_MAXHEALTH", function() if RAID_UNIT_LOOKUP[arg1] then self:UpdateHealth(arg1) end end) + SFrames:RegisterEvent("UNIT_MANA", function() if RAID_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end) + SFrames:RegisterEvent("UNIT_MAXMANA", function() if RAID_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end) + SFrames:RegisterEvent("UNIT_AURA", function() if RAID_UNIT_LOOKUP[arg1] then self:UpdateAuras(arg1) end end) + SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcons() end) + + self:UpdateAll() +end + +function SFrames.Raid:EnsureFrames() + if self._framesBuilt then return end + self._framesBuilt = true + + for i = 1, 40 do + local unit = "raid" .. i + local groupIndex = math.ceil(i / 5) + local groupParent = self.groups[groupIndex] + local f = CreateFrame("Button", "SFramesRaidFrame"..i, groupParent) + + f.id = i + f:RegisterForClicks("LeftButtonUp", "RightButtonUp") + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() + if IsAltKeyDown() or SFrames.isUnlocked then + SFrames.Raid.parent:StartMoving() + end + end) + f:SetScript("OnDragStop", function() + SFrames.Raid.parent:StopMovingOrSizing() + SFrames.Raid:SavePosition() + end) + f:SetScript("OnClick", function() + if arg1 == "LeftButton" then + if TryDropCursorOnUnit(this.unit) then return end + if SpellIsTargeting and SpellIsTargeting() then + SpellTargetUnit(this.unit) + return + end + TargetUnit(this.unit) + elseif arg1 == "RightButton" then + if UnitExists(this.unit) then + GameTooltip:Hide() + if not SFrames.Raid.dropDown then + SFrames.Raid.dropDown = CreateFrame("Frame", "SFramesRaidDropDown", UIParent, "UIDropDownMenuTemplate") + SFrames.Raid.dropDown.displayMode = "MENU" + SFrames.Raid.dropDown.initialize = function() + local dd = SFrames.Raid.dropDown + local unit = dd.unit + local name = dd.name + if not unit or not name then return end + + local info = {} + info.text = name + info.isTitle = 1 + info.notCheckable = 1 + UIDropDownMenu_AddButton(info) + + if not UnitIsUnit(unit, "player") then + info = {} + info.text = WHISPER or "密语" + info.notCheckable = 1 + info.func = function() ChatFrame_SendTell(name) end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = INSPECT or "检查" + info.notCheckable = 1 + info.func = function() InspectUnit(unit) end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = TRADE or "交易" + info.notCheckable = 1 + info.func = function() InitiateTrade(unit) end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = FOLLOW or "跟随" + info.notCheckable = 1 + info.func = function() FollowUnit(unit) end + UIDropDownMenu_AddButton(info) + end + + if (IsRaidLeader and IsRaidLeader()) or (IsRaidOfficer and IsRaidOfficer()) then + if not UnitIsUnit(unit, "player") then + info = {} + info.text = "提升为助手" + info.notCheckable = 1 + info.func = function() PromoteToAssistant(name) end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = "移出团队" + info.notCheckable = 1 + info.func = function() UninviteByName(name) end + UIDropDownMenu_AddButton(info) + end + end + + info = {} + info.text = CANCEL or "取消" + info.notCheckable = 1 + UIDropDownMenu_AddButton(info) + end + end + SFrames.Raid.dropDown.unit = this.unit + SFrames.Raid.dropDown.name = UnitName(this.unit) + ToggleDropDownMenu(1, nil, SFrames.Raid.dropDown, "cursor") + end + end + end) + f:SetScript("OnReceiveDrag", function() + if TryDropCursorOnUnit(this.unit) then return end + if SpellIsTargeting and SpellIsTargeting() then + SpellTargetUnit(this.unit) + end + end) + f:SetScript("OnEnter", function() + GameTooltip_SetDefaultAnchor(GameTooltip, this) + GameTooltip:SetUnit(this.unit) + end) + f:SetScript("OnLeave", function() GameTooltip:Hide() end) + f.unit = unit + + -- Health Bar + f.health = SFrames:CreateStatusBar(f, "SFramesRaidFrame"..i.."Health") + + local hbg = CreateFrame("Frame", nil, f) + hbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(hbg) + f.healthBGFrame = hbg + + f.health.bg = f.health:CreateTexture(nil, "BACKGROUND") + f.health.bg:SetAllPoints() + f.health.bg:SetTexture(SFrames:GetTexture()) + f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + -- Heal prediction overlay (incoming heals) + f.health.healPredMine = f.health:CreateTexture(nil, "OVERLAY") + f.health.healPredMine:SetTexture(SFrames:GetTexture()) + f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78) + f.health.healPredMine:Hide() + + f.health.healPredOther = f.health:CreateTexture(nil, "OVERLAY") + f.health.healPredOther:SetTexture(SFrames:GetTexture()) + f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5) + f.health.healPredOther:Hide() + + -- Power Bar + f.power = SFrames:CreateStatusBar(f, "SFramesRaidFrame"..i.."Power") + f.power:SetMinMaxValues(0, 100) + + local powerbg = CreateFrame("Frame", nil, f) + powerbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(powerbg) + f.powerBGFrame = powerbg + + f.power.bg = f.power:CreateTexture(nil, "BACKGROUND") + f.power.bg:SetAllPoints() + f.power.bg:SetTexture(SFrames:GetTexture()) + f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + -- Target Highlight + f.targetHighlight = f:CreateTexture(nil, "OVERLAY") + f.targetHighlight:SetPoint("TOPLEFT", f, "TOPLEFT", -2, 2) + f.targetHighlight:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 2, -2) + f.targetHighlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + f.targetHighlight:SetBlendMode("ADD") + f.targetHighlight:SetVertexColor(1, 1, 1, 0.6) + f.targetHighlight:Hide() + + -- Texts + f.nameText = SFrames:CreateFontString(f.health, 10, "CENTER") + f.nameText:SetPoint("TOP", f.health, "TOP", 0, -2) + f.nameText:SetShadowColor(0, 0, 0, 1) + f.nameText:SetShadowOffset(1, -1) + + f.healthText = SFrames:CreateFontString(f.health, 9, "CENTER") + f.healthText:SetPoint("BOTTOM", f.health, "BOTTOM", 0, 2) + f.healthText:SetShadowColor(0, 0, 0, 1) + f.healthText:SetShadowOffset(1, -1) + f.healthText:SetTextColor(0.8, 0.8, 0.8) + + local offOvr = CreateFrame("Frame", nil, f) + offOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 4) + offOvr:SetAllPoints(f) + local offIco = SFrames:CreateIcon(offOvr, "offline", 16) + offIco:SetPoint("CENTER", offOvr, "CENTER", 0, 0) + offIco:SetVertexColor(0.7, 0.7, 0.7, 0.9) + offIco:Hide() + f.offlineIcon = offIco + + local roleOvr = CreateFrame("Frame", nil, f) + roleOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 6) + roleOvr:SetAllPoints(f) + + f.leaderIcon = roleOvr:CreateTexture(nil, "OVERLAY") + f.leaderIcon:SetWidth(12) + f.leaderIcon:SetHeight(12) + f.leaderIcon:SetPoint("TOPLEFT", f, "TOPLEFT", -3, 3) + f.leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-LeaderIcon") + f.leaderIcon:Hide() + + f.assistantIcon = roleOvr:CreateFontString(nil, "OVERLAY") + f.assistantIcon:SetFont(SFrames:GetFont(), 12, "OUTLINE") + f.assistantIcon:SetPoint("TOPLEFT", f, "TOPLEFT", -1, 1) + f.assistantIcon:SetTextColor(0.5, 0.8, 1.0) + f.assistantIcon:SetShadowColor(0, 0, 0, 1) + f.assistantIcon:SetShadowOffset(1, -1) + f.assistantIcon:SetText("+") + f.assistantIcon:Hide() + + -- Raid Target Icon + local raidIconSize = 16 + local raidIconOvr = CreateFrame("Frame", nil, f) + raidIconOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 7) + raidIconOvr:SetWidth(raidIconSize) + raidIconOvr:SetHeight(raidIconSize) + raidIconOvr:SetPoint("CENTER", f.health, "TOP", 0, 0) + f.raidIcon = raidIconOvr:CreateTexture(nil, "OVERLAY") + f.raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons") + f.raidIcon:SetAllPoints(raidIconOvr) + f.raidIcon:Hide() + f.raidIconOverlay = raidIconOvr + + f.indicators = {} + for pos = 1, 4 do + local ind = CreateFrame("Button", nil, f) + ind:SetWidth(10) + ind:SetHeight(10) + ind:SetFrameLevel(f:GetFrameLevel() + 5) + SFrames:CreateUnitBackdrop(ind) + + ind.icon = ind:CreateTexture(nil, "ARTWORK") + ind.icon:SetAllPoints() + ind.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + + if pos == 1 then + ind:SetPoint("TOPLEFT", f.health, "TOPLEFT", 1, -1) + elseif pos == 2 then + ind:SetPoint("TOPRIGHT", f.health, "TOPRIGHT", -1, -1) + elseif pos == 3 then + ind:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", 1, 1) + elseif pos == 4 then + ind:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", -1, 1) + end + + ind:SetScript("OnEnter", function() + if this.index then + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") + if this.isDebuff then + GameTooltip:SetUnitDebuff(f.unit, this.index) + else + GameTooltip:SetUnitBuff(f.unit, this.index) + end + end + end) + ind:SetScript("OnLeave", function() GameTooltip:Hide() end) + + ind:Hide() + f.indicators[pos] = ind + end + + self.frames[i] = { frame = f, unit = unit, index = i } + + self:ApplyFrameStyle(f, self:GetMetrics()) + + f.rangeTimer = 0 + f.auraScanTimer = 0 + f.healPredTimer = 0 + + f:SetScript("OnShow", function() + if not SFrames.Raid.testing then + SFrames.Raid:UpdateFrame(this.unit) + end + end) + + f:Hide() + end + + if not self._globalUpdateFrame then + self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent) + self._globalUpdateFrame:SetScript("OnUpdate", function() + if SFrames.Raid.testing then return end + local dt = arg1 + local frames = SFrames.Raid.frames + if not frames then return end + for i = 1, 40 do + local entry = frames[i] + if entry then + local f = entry.frame + if f:IsVisible() and f.unit and f.unit ~= "" then + f.rangeTimer = f.rangeTimer + dt + if f.rangeTimer >= 0.5 then + if UnitExists(f.unit) and not CheckInteractDistance(f.unit, 4) then + f:SetAlpha(0.6) + else + f:SetAlpha(1.0) + end + f.rangeTimer = 0 + end + + f.auraScanTimer = f.auraScanTimer + dt + if f.auraScanTimer >= 0.7 then + SFrames.Raid:UpdateAuras(f.unit) + f.auraScanTimer = 0 + end + + f.healPredTimer = f.healPredTimer + dt + if f.healPredTimer >= 0.2 then + SFrames.Raid:UpdateHealPrediction(f.unit) + f.healPredTimer = 0 + end + end + end + end + end) + end + + self:ApplyLayout() +end + +function SFrames.Raid:UpdateAll() + if self.testing then return end + + if SFramesDB and SFramesDB.enableRaidFrames == false then + self.parent:Hide() + return + else + self.parent:Show() + end + + if GetNumRaidMembers() > 0 then + self:EnsureFrames() + for i = 1, 40 do + self.frames[i].frame:Hide() + self.frames[i].frame.unit = "" + self.frames[i].unit = "" + end + + local groupSlots = {} + for g = 1, 8 do + groupSlots[g] = 0 + end + + for i = 1, 40 do + local name, rank, subgroup = GetRaidRosterInfo(i) + if name and subgroup and subgroup >= 1 and subgroup <= 8 then + groupSlots[subgroup] = groupSlots[subgroup] + 1 + local slot = groupSlots[subgroup] + if slot <= 5 then + local frameIndex = (subgroup - 1) * 5 + slot + local f = self.frames[frameIndex].frame + f.unit = "raid" .. i + f.raidRank = rank + self.frames[frameIndex].unit = "raid" .. i + f:Show() + self:UpdateFrame("raid" .. i) + end + end + end + + -- Show/hide group labels based on whether each group has members + local showLabel = SFramesDB and SFramesDB.raidShowGroupLabel ~= false + for g = 1, 8 do + local lbl = self.groupLabels and self.groupLabels[g] + if lbl then + if showLabel and groupSlots[g] > 0 then + lbl:Show() + else + lbl:Hide() + end + end + end + else + if not self.testing and self._framesBuilt then + for i = 1, 40 do + self.frames[i].frame:Hide() + self.frames[i].frame.unit = "" + self.frames[i].unit = "" + end + end + for g = 1, 8 do + local lbl = self.groupLabels and self.groupLabels[g] + if lbl then lbl:Hide() end + end + end +end + +function SFrames.Raid:UpdateFrame(unit) + if self.testing or not self._framesBuilt then return end + for i=1, 40 do + if self.frames[i].unit == unit then + local f = self.frames[i].frame + local name = UnitName(unit) or "" + local _, class = UnitClass(unit) + + local display = string.sub(name, 1, 15) + f.nameText:SetText(display) + + local rank = f.raidRank or 0 + if f.leaderIcon then + if rank == 2 then f.leaderIcon:Show() else f.leaderIcon:Hide() end + end + if f.assistantIcon then + if rank == 1 then f.assistantIcon:Show() else f.assistantIcon:Hide() end + end + + if not UnitIsConnected(unit) then + f.health:SetStatusBarColor(0.5, 0.5, 0.5) + f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + f.nameText:SetTextColor(0.5, 0.5, 0.5) + if f.offlineIcon then f.offlineIcon:Show() end + else + if f.offlineIcon then f.offlineIcon:Hide() end + if class and SFrames.Config.colors.class[class] then + local color = SFrames.Config.colors.class[class] + f.health:SetStatusBarColor(color.r, color.g, color.b) + f.nameText:SetTextColor(1, 1, 1) + else + f.health:SetStatusBarColor(0, 1, 0) + f.nameText:SetTextColor(1, 1, 1) + end + end + + self:UpdateHealth(unit) + self:UpdatePower(unit) + self:UpdateAuras(unit) + self:UpdateRaidIcon(unit) + self:UpdateTargetHighlight() + break + end + end +end + +function SFrames.Raid:UpdateTargetHighlight() + if self.testing or not self._framesBuilt then return end + for i=1, 40 do + local f = self.frames[i].frame + if UnitExists("target") and UnitIsUnit("target", self.frames[i].unit) then + f.targetHighlight:Show() + else + f.targetHighlight:Hide() + end + end +end + +function SFrames.Raid:UpdateHealth(unit) + if not self._framesBuilt then return end + for i=1, 40 do + if self.frames[i].unit == unit then + local f = self.frames[i].frame + if not UnitIsConnected(unit) then + f.health:SetMinMaxValues(0, 100) + f.health:SetValue(0) + f.healthText:SetText("离线") + if f.offlineIcon then f.offlineIcon:Show() end + return + end + if f.offlineIcon then f.offlineIcon:Hide() end + + local hp = UnitHealth(unit) + local maxHp = UnitHealthMax(unit) + + if UnitIsDead(unit) then + f.health:SetMinMaxValues(0, 100) + f.health:SetValue(0) + f.healthText:SetText("死亡") + f.nameText:SetTextColor(0.5, 0.5, 0.5) + return + end + + if UnitIsGhost(unit) then + f.health:SetMinMaxValues(0, 100) + f.health:SetValue(0) + f.healthText:SetText("灵魂") + f.nameText:SetTextColor(0.5, 0.5, 0.5) + return + end + + f.health:SetMinMaxValues(0, maxHp) + f.health:SetValue(hp) + + local percent = 0 + if maxHp > 0 then + percent = math.floor((hp / maxHp) * 100) + end + + local db = SFramesDB or {} + local txt = "" + if db.raidHealthFormat == "percent" then + txt = percent .. "%" + elseif db.raidHealthFormat == "deficit" then + if maxHp - hp > 0 then + txt = "-" .. (maxHp - hp) + end + else + txt = (math.floor(hp/100)/10).."k" -- default compact e.g. 4.5k + if hp < 1000 then txt = tostring(hp) end + end + + f.healthText:SetText(txt) + self:UpdateHealPrediction(unit) + break + end + end +end + +function SFrames.Raid:UpdateHealPrediction(unit) + if not self._framesBuilt then return end + local frameData = nil + for i=1, 40 do + if self.frames[i].unit == unit then + frameData = self.frames[i] + break + end + end + if not frameData then return end + + local f = frameData.frame + if not (f.health and f.health.healPredMine and f.health.healPredOther) then return end + + local predMine = f.health.healPredMine + local predOther = f.health.healPredOther + + local function HidePredictions() + predMine:Hide() + predOther:Hide() + end + + if not UnitExists(unit) or not UnitIsConnected(unit) then + HidePredictions() + return + end + + local hp = UnitHealth(unit) or 0 + local maxHp = UnitHealthMax(unit) or 0 + if maxHp <= 0 or hp >= maxHp then + HidePredictions() + return + end + + local _, mineIncoming, othersIncoming = GetIncomingHeals(unit) + local missing = maxHp - hp + if missing <= 0 then + HidePredictions() + return + end + + local mineShown = math.min(math.max(0, mineIncoming), missing) + local remaining = missing - mineShown + local otherShown = math.min(math.max(0, othersIncoming), remaining) + if mineShown <= 0 and otherShown <= 0 then + HidePredictions() + return + end + + local barWidth = f.health:GetWidth() or 0 + if barWidth <= 0 then + HidePredictions() + return + end + + local currentWidth = math.floor((hp / maxHp) * barWidth + 0.5) + if currentWidth < 0 then currentWidth = 0 end + if currentWidth > barWidth then currentWidth = barWidth end + + local availableWidth = barWidth - currentWidth + if availableWidth <= 0 then + HidePredictions() + return + end + + local mineWidth = math.floor((mineShown / maxHp) * barWidth + 0.5) + local otherWidth = math.floor((otherShown / maxHp) * barWidth + 0.5) + if mineWidth < 0 then mineWidth = 0 end + if otherWidth < 0 then otherWidth = 0 end + if mineWidth > availableWidth then mineWidth = availableWidth end + if otherWidth > (availableWidth - mineWidth) then + otherWidth = availableWidth - mineWidth + end + + if mineWidth > 0 then + predMine:ClearAllPoints() + predMine:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth, 0) + predMine:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth, 0) + predMine:SetWidth(mineWidth) + predMine:Show() + else + predMine:Hide() + end + + if otherWidth > 0 then + predOther:ClearAllPoints() + predOther:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth + mineWidth, 0) + predOther:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth + mineWidth, 0) + predOther:SetWidth(otherWidth) + predOther:Show() + else + predOther:Hide() + end +end + +function SFrames.Raid:UpdatePower(unit) + if not self._framesBuilt then return end + local db = SFramesDB or {} + if db.raidShowPower == false then return end + + for i=1, 40 do + if self.frames[i].unit == unit then + local f = self.frames[i].frame + local power = UnitMana(unit) + local maxPower = UnitManaMax(unit) + local _, class = UnitClass(unit) + + f.power:SetMinMaxValues(0, maxPower) + f.power:SetValue(power) + + local pType = UnitPowerType(unit) + local color = SFrames.Config.colors.power[pType] or SFrames.Config.colors.power[0] + f.power:SetStatusBarColor(color.r, color.g, color.b) + break + end + end +end + +function SFrames.Raid:UpdateRaidIcons() + if not self._framesBuilt then return end + for i = 1, 40 do + local unit = self.frames[i].unit + if unit and unit ~= "" and self.frames[i].frame:IsShown() then + self:UpdateRaidIcon(unit) + end + end +end + +function SFrames.Raid:UpdateRaidIcon(unit) + if not self._framesBuilt then return end + for i = 1, 40 do + if self.frames[i].unit == unit then + local f = self.frames[i].frame + if not f.raidIcon then return end + if not GetRaidTargetIndex then + f.raidIcon:Hide() + return + end + if not UnitExists(unit) then + f.raidIcon:Hide() + return + end + local index = GetRaidTargetIndex(unit) + if index and index > 0 and index <= 8 then + local col = math.mod(index - 1, 4) + local row = math.floor((index - 1) / 4) + f.raidIcon:SetTexCoord(col * 0.25, (col + 1) * 0.25, row * 0.25, (row + 1) * 0.25) + f.raidIcon:Show() + else + f.raidIcon:Hide() + end + break + end + end +end + +local PlayerClassBuffs = nil + +function SFrames.Raid:GetClassBuffs() + if PlayerClassBuffs then return PlayerClassBuffs end + + local _, playerClass = UnitClass("player") + PlayerClassBuffs = {} + + if playerClass == "PRIEST" then + PlayerClassBuffs = { + [1] = { "真言术:韧", "坚韧祷言" }, + [2] = { "神圣之灵", "精神祷言" }, + [3] = { "恢复" }, + [4] = { "虚弱灵魂", isDebuff = true }, + [5] = { "防恐结界" } + } + elseif playerClass == "DRUID" then + PlayerClassBuffs = { + [1] = { "野性印记", "野性赐福" }, + [2] = { "荆棘术" }, + [3] = { "回春术" }, + [4] = { "愈合" }, + } + elseif playerClass == "PALADIN" then + PlayerClassBuffs = { + [1] = { "王者祝福", "强效王者祝福" }, + [2] = { "拯救祝福", "强效拯救祝福" }, + [3] = { "智慧祝福", "强效智慧祝福" }, + [4] = { "力量祝福", "强效力量祝福" }, + [5] = { "庇护祝福", "强效庇护祝福" }, + [6] = { "光明祝福", "强效光明祝福" } + } + elseif playerClass == "MAGE" then + PlayerClassBuffs = { + [1] = { "奥术智慧", "奥术光辉" }, + [2] = { "魔法抑制" }, + [3] = { "魔法增效" } + } + end + + return PlayerClassBuffs +end + +function SFrames.Raid:UpdateAuras(unit) + if not self._framesBuilt then return end + local frameData = nil + for i=1, 40 do + if self.frames[i].unit == unit then + frameData = self.frames[i] + break + end + end + if not frameData then return end + + local f = frameData.frame + local buffsNeeded = self:GetClassBuffs() + + local foundIndicators = { [1] = false, [2] = false, [3] = false, [4] = false } + + -- Hide all first + for i = 1, 4 do + f.indicators[i]:Hide() + f.indicators[i].index = nil + end + + if not buffsNeeded or not UnitIsConnected(unit) or UnitIsDeadOrGhost(unit) then + f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + return + end + + local function MatchesList(auraName, list) + for _, name in ipairs(list) do + if string.find(auraName, name) then + return true + end + end + return false + end + + -- Check Buffs + for i = 1, 32 do + local texture, applications = UnitBuff(unit, i) + if not texture then break end + + SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") + SFrames.Tooltip:SetUnitBuff(unit, i) + local buffName = SFramesScanTooltipTextLeft1:GetText() + + if buffName then + for pos, listData in pairs(buffsNeeded) do + if pos <= 4 and not listData.isDebuff and not foundIndicators[pos] then + if MatchesList(buffName, listData) then + f.indicators[pos].icon:SetTexture(texture) + f.indicators[pos].index = i + f.indicators[pos].isDebuff = false + f.indicators[pos]:Show() + foundIndicators[pos] = true + end + end + end + end + end + + local hasDebuff = false + local debuffColor = {r=_A.slotBg[1], g=_A.slotBg[2], b=_A.slotBg[3]} + + -- Check Debuffs + for i = 1, 16 do + local texture, applications, dispelType = UnitDebuff(unit, i) + if not texture then break end + + if dispelType then + hasDebuff = true + if dispelType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1} + elseif dispelType == "Curse" then debuffColor = {r=0.6, g=0, b=1} + elseif dispelType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0} + elseif dispelType == "Poison" then debuffColor = {r=0, g=0.6, b=0} + end + end + + SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") + SFrames.Tooltip:SetUnitDebuff(unit, i) + local debuffName = SFramesScanTooltipTextLeft1:GetText() + + if debuffName then + for pos, listData in pairs(buffsNeeded) do + if pos <= 4 and listData.isDebuff and not foundIndicators[pos] then + if MatchesList(debuffName, listData) then + f.indicators[pos].icon:SetTexture(texture) + f.indicators[pos].index = i + f.indicators[pos].isDebuff = true + f.indicators[pos]:Show() + foundIndicators[pos] = true + end + end + end + end + end + + if hasDebuff then + f.health.bg:SetVertexColor(debuffColor.r, debuffColor.g, debuffColor.b, 1) + else + f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + end +end + diff --git a/Units/TalentTree.lua b/Units/TalentTree.lua new file mode 100644 index 0000000..7217f27 --- /dev/null +++ b/Units/TalentTree.lua @@ -0,0 +1,1065 @@ +SFrames.TalentTree = {} + +local ICON_SIZE = 36 +local ICON_SPACING_X = 14 +local ICON_SPACING_Y = 14 +local TAB_WIDTH = 220 +local FRAME_WIDTH = (TAB_WIDTH * 3) + 40 +local FRAME_HEIGHT = 520 + +-------------------------------------------------------------------------------- +-- Theme: Pink Cat-Paw +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend() +local function GetHex() + return (SFrames.Theme and SFrames.Theme:GetAccentHex()) or "ffffb3d9" +end + +-------------------------------------------------------------------------------- +-- Class definitions +-------------------------------------------------------------------------------- +local CLASS_LIST = { + { key = "WARRIOR", name = "战士", color = { 0.78, 0.61, 0.43 } }, + { key = "PALADIN", name = "圣骑士", color = { 0.96, 0.55, 0.73 } }, + { key = "HUNTER", name = "猎人", color = { 0.67, 0.83, 0.45 } }, + { key = "ROGUE", name = "盗贼", color = { 1.00, 0.96, 0.41 } }, + { key = "PRIEST", name = "牧师", color = { 1.00, 1.00, 1.00 } }, + { key = "SHAMAN", name = "萨满", color = { 0.00, 0.44, 0.87 } }, + { key = "MAGE", name = "法师", color = { 0.41, 0.80, 0.94 } }, + { key = "WARLOCK", name = "术士", color = { 0.58, 0.51, 0.79 } }, + { key = "DRUID", name = "德鲁伊", color = { 1.00, 0.49, 0.04 } }, +} + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + local bg = bgColor or T.panelBg + local bd = borderColor or T.panelBorder + frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) + frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) +end + +local function SetPixelBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 } + }) + if bgColor then frame:SetBackdropColor(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1) end + if borderColor then frame:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1) end +end + +local function CreateShadow(parent, size) + local s = CreateFrame("Frame", nil, parent) + local sz = size or 4 + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz) + s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0)) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.55) + s:SetBackdropBorderColor(0, 0, 0, 0.4) + return s +end + +local function MakeFS(parent, size, justifyH, color) + local fs = parent:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), size or 11, "OUTLINE") + fs:SetJustifyH(justifyH or "LEFT") + local c = color or T.valueText + fs:SetTextColor(c[1], c[2], c[3]) + return fs +end + +local function StyleButton(btn, label) + SetRoundBackdrop(btn, T.btnBg, T.btnBorder) + + local fs = MakeFS(btn, 12, "CENTER", T.btnText) + fs:SetPoint("CENTER", btn, "CENTER", 0, 0) + if label then fs:SetText(label) end + btn.nanamiLabel = fs + + btn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + if this.nanamiLabel then + this.nanamiLabel:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end + if this.nanamiTooltip then + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:AddLine(this.nanamiTooltip[1], 1, 1, 1) + if this.nanamiTooltip[2] then + GameTooltip:AddLine(this.nanamiTooltip[2], 0.7, 0.7, 0.7) + end + GameTooltip:Show() + end + end) + + btn:SetScript("OnLeave", function() + this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + if this.nanamiLabel then + this.nanamiLabel:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + GameTooltip:Hide() + end) + + btn:SetScript("OnMouseDown", function() + this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) + end) + + btn:SetScript("OnMouseUp", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + end) + + return fs +end + +-------------------------------------------------------------------------------- +-- Talent data cache (stores talent trees per class in SFramesGlobalDB) +-------------------------------------------------------------------------------- +local function GetCache() + if not SFramesGlobalDB then SFramesGlobalDB = {} end + if not SFramesGlobalDB.talentCache then SFramesGlobalDB.talentCache = {} end + return SFramesGlobalDB.talentCache +end + +local function CacheCurrentClassData() + local _, classEn = UnitClass("player") + if not classEn then return end + local numTabs = GetNumTalentTabs() + if numTabs == 0 then return end + + local cache = GetCache() + local classData = {} + + for t = 1, numTabs do + local tabName, tabIcon, pointsSpent, background = GetTalentTabInfo(t) + local treeData = { name = tabName, icon = tabIcon, background = background or "", talents = {}, numTalents = 0 } + local numTalents = GetNumTalents(t) + treeData.numTalents = numTalents + + for i = 1, numTalents do + local tName, tIcon, tier, column, rank, maxRank = GetTalentInfo(t, i) + local prereqTier, prereqCol = GetTalentPrereqs(t, i) + + local descLines = {} + local scanTip = _G["NanamiTalentScanTip"] + if not scanTip then + scanTip = CreateFrame("GameTooltip", "NanamiTalentScanTip", UIParent, "GameTooltipTemplate") + scanTip:SetOwner(UIParent, "ANCHOR_NONE") + end + scanTip:ClearLines() + scanTip:SetTalent(t, i) + local numLines = scanTip:NumLines() + for li = 2, numLines do + local lineObj = _G["NanamiTalentScanTipTextLeft" .. li] + if lineObj then + local txt = lineObj:GetText() + if txt and txt ~= "" then + table.insert(descLines, txt) + end + end + end + + treeData.talents[i] = { + name = tName or "", + icon = tIcon or "", + tier = tier or 1, + column = column or 1, + maxRank = maxRank or 1, + prereqTier = prereqTier, + prereqColumn = prereqCol, + desc = descLines, + } + end + classData[t] = treeData + end + classData.numTabs = numTabs + cache[classEn] = classData +end + +-------------------------------------------------------------------------------- +-- Data access wrappers (API for own class, cache for others) +-------------------------------------------------------------------------------- +local TT = SFrames.TalentTree + +local function IsViewingOwnClass(self) + return not self.viewingClass or self.viewingClass == self.playerClass +end + +local function TT_GetNumTabs(self) + if IsViewingOwnClass(self) then return GetNumTalentTabs() end + local cache = GetCache() + local cd = cache[self.viewingClass] + return cd and cd.numTabs or 0 +end + +local function TT_GetTabInfo(self, tab) + if IsViewingOwnClass(self) then return GetTalentTabInfo(tab) end + local cache = GetCache() + local cd = cache[self.viewingClass] + if cd and cd[tab] then + return cd[tab].name, cd[tab].icon, 0, cd[tab].background + end + return "", "", 0, "" +end + +local function TT_GetNumTalents(self, tab) + if IsViewingOwnClass(self) then return GetNumTalents(tab) end + local cache = GetCache() + local cd = cache[self.viewingClass] + if cd and cd[tab] then return cd[tab].numTalents or 0 end + return 0 +end + +local function TT_GetTalentInfo(self, tab, index) + if IsViewingOwnClass(self) then return GetTalentInfo(tab, index) end + local cache = GetCache() + local cd = cache[self.viewingClass] + if cd and cd[tab] and cd[tab].talents[index] then + local t = cd[tab].talents[index] + return t.name, t.icon, t.tier, t.column, 0, t.maxRank, nil, true + end + return nil +end + +local function TT_GetTalentPrereqs(self, tab, index) + if IsViewingOwnClass(self) then return GetTalentPrereqs(tab, index) end + local cache = GetCache() + local cd = cache[self.viewingClass] + if cd and cd[tab] and cd[tab].talents[index] then + local t = cd[tab].talents[index] + return t.prereqTier, t.prereqColumn + end + return nil, nil +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function SFrames.TalentTree:Initialize() + local _, classEn = UnitClass("player") + self.playerClass = classEn + + self:CreateMainFrame() + self:HookVanillaUI() + + SFrames:RegisterEvent("CHARACTER_POINTS_CHANGED", function() + if self.frame and self.frame:IsShown() then self:Update() end + end) + SFrames:RegisterEvent("SPELLS_CHANGED", function() + if self.frame and self.frame:IsShown() then self:Update() end + end) +end + +-------------------------------------------------------------------------------- +-- Main Frame +-------------------------------------------------------------------------------- +function SFrames.TalentTree:CreateMainFrame() + local f = CreateFrame("Frame", "SFramesTalentFrame", UIParent) + f:SetWidth(FRAME_WIDTH) + f:SetHeight(FRAME_HEIGHT) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + SetRoundBackdrop(f, T.panelBg, T.panelBorder) + CreateShadow(f, 5) + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + f:SetFrameStrata("HIGH") + f:Hide() + + local titleIcoSize = 14 + local titleGap = 4 + f.title = MakeFS(f, 14, "CENTER", T.titleColor) + f.title:SetPoint("TOP", f, "TOP", (titleIcoSize + titleGap) / 2, -10) + f.title:SetText("|c" .. GetHex() .. "Nanami|r 天赋系统") + local titleIco = SFrames:CreateIcon(f, "talent", titleIcoSize) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetVertexColor(T.titleColor[1], T.titleColor[2], T.titleColor[3]) + titleIco:SetPoint("RIGHT", f.title, "LEFT", -titleGap, 0) + + -- Close button + local closeBtn = CreateFrame("Button", nil, f) + closeBtn:SetWidth(18) + closeBtn:SetHeight(18) + closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8) + closeBtn:SetFrameLevel(f:GetFrameLevel() + 3) + SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder) + local closeTxt = MakeFS(closeBtn, 10, "CENTER", T.title) + closeTxt:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) + closeTxt:SetText("x") + closeBtn:SetScript("OnClick", function() f:Hide() end) + closeBtn:SetScript("OnEnter", function() + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) + end) + closeBtn:SetScript("OnLeave", function() + this:SetBackdropColor(T.buttonDownBg[1], T.buttonDownBg[2], T.buttonDownBg[3], T.buttonDownBg[4]) + this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + end) + f.close = closeBtn + + -- Class selector bar (shown in sim mode) + local classBar = CreateFrame("Frame", nil, f) + classBar:SetHeight(20) + classBar:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -28) + classBar:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -28) + classBar:SetFrameLevel(f:GetFrameLevel() + 2) + classBar:Hide() + f.classBar = classBar + + local numClasses = table.getn(CLASS_LIST) + local gap = 2 + local barW = FRAME_WIDTH - 20 + local cbW = math.floor((barW - (numClasses - 1) * gap) / numClasses) + f.classBtns = {} + + for ci, cinfo in ipairs(CLASS_LIST) do + local cb = CreateFrame("Button", nil, classBar) + cb:SetWidth(cbW) + cb:SetHeight(18) + cb:SetPoint("TOPLEFT", classBar, "TOPLEFT", (ci - 1) * (cbW + gap), 0) + SetPixelBackdrop(cb, T.slotBg, T.slotBorder) + + local cIcon = SFrames:CreateClassIcon(cb, 14) + cIcon.overlay:SetPoint("LEFT", cb, "LEFT", 3, 0) + SFrames:SetClassIcon(cIcon, cinfo.key) + cb.classIconTex = cIcon + + local cbt = MakeFS(cb, 9, "CENTER", cinfo.color) + cbt:SetPoint("LEFT", cIcon.overlay, "RIGHT", 1, 0) + cbt:SetPoint("RIGHT", cb, "RIGHT", -2, 0) + cbt:SetText(cinfo.name) + cb.label = cbt + cb.classKey = cinfo.key + cb.classColor = cinfo.color + cb:SetScript("OnClick", function() + SFrames.TalentTree:SwitchViewClass(this.classKey) + end) + cb:SetScript("OnEnter", function() + this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4]) + local cache = GetCache() + if not cache[this.classKey] and this.classKey ~= SFrames.TalentTree.playerClass then + GameTooltip:SetOwner(this, "ANCHOR_BOTTOM") + GameTooltip:AddLine("需先用该职业角色打开天赋面板以缓存数据", 1, 0.5, 0.5) + GameTooltip:Show() + end + end) + cb:SetScript("OnLeave", function() + this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + GameTooltip:Hide() + end) + f.classBtns[ci] = cb + end + + -- Overlay for text on top of children + f.overlay = CreateFrame("Frame", nil, f) + f.overlay:SetAllPoints(f) + f.overlay:SetFrameLevel(f:GetFrameLevel() + 10) + + f.pointsText = MakeFS(f.overlay, 13, "LEFT", T.titleColor) + f.pointsText:SetPoint("BOTTOMLEFT", f.overlay, "BOTTOMLEFT", 14, 25) + + f.simLabel = MakeFS(f.overlay, 11, "LEFT", T.titleColor) + f.simLabel:SetPoint("LEFT", f.pointsText, "LEFT", 0, -18) + f.simLabel:SetText("") + + -- Bottom buttons + f.btnApply = CreateFrame("Button", nil, f) + f.btnApply:SetWidth(100) + f.btnApply:SetHeight(26) + f.btnApply:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 12) + StyleButton(f.btnApply, "应用天赋") + f.btnApply.nanamiTooltip = { "应用天赋", "将所有模拟点数提交至服务器。" } + f.btnApply:SetScript("OnClick", function() SFrames.TalentTree:ApplyVirtualPoints() end) + + f.btnReset = CreateFrame("Button", nil, f) + f.btnReset:SetWidth(100) + f.btnReset:SetHeight(26) + f.btnReset:SetPoint("RIGHT", f.btnApply, "LEFT", -8, 0) + StyleButton(f.btnReset, "重置预览") + f.btnReset:SetScript("OnClick", function() SFrames.TalentTree:ResetVirtualPoints() end) + + f.btnSimMode = CreateFrame("Button", nil, f) + f.btnSimMode:SetWidth(110) + f.btnSimMode:SetHeight(26) + f.btnSimMode:SetPoint("RIGHT", f.btnReset, "LEFT", -8, 0) + StyleButton(f.btnSimMode, "|cff888888模拟加点: 关|r") + f.simModeText = f.btnSimMode.nanamiLabel + f.btnSimMode.nanamiTooltip = { "模拟加点模式", "开启后可用左键虚拟加点、右键取消,\n确认后点「应用天赋」才会实际扣分。\n可切换职业模拟其他职业加点。" } + f.btnSimMode:SetScript("OnClick", function() + SFrames.TalentTree.simMode = not SFrames.TalentTree.simMode + SFrames.TalentTree:UpdateSimModeLabel() + end) + + self.frame = f + self.tabs = {} + self.virtualPoints = {} + self.simMode = false + self.viewingClass = nil + + tinsert(UISpecialFrames, "SFramesTalentFrame") +end + +-------------------------------------------------------------------------------- +-- Class bar update +-------------------------------------------------------------------------------- +function SFrames.TalentTree:UpdateClassBar() + if not self.frame or not self.frame.classBtns then return end + local viewing = self.viewingClass or self.playerClass + local cache = GetCache() + + for ci, cinfo in ipairs(CLASS_LIST) do + local cb = self.frame.classBtns[ci] + if not cb then break end + + local hasData = (cinfo.key == self.playerClass) or (cache[cinfo.key] ~= nil) + + if cinfo.key == viewing then + SetPixelBackdrop(cb, T.tabActiveBg, cinfo.color) + cb.label:SetTextColor(cinfo.color[1], cinfo.color[2], cinfo.color[3]) + elseif hasData then + SetPixelBackdrop(cb, T.slotBg, T.slotBorder) + cb.label:SetTextColor(cinfo.color[1] * 0.8, cinfo.color[2] * 0.8, cinfo.color[3] * 0.8) + else + SetPixelBackdrop(cb, T.emptySlotBg, T.emptySlotBd) + cb.label:SetTextColor(T.passive[1], T.passive[2], T.passive[3]) + end + end +end + +function SFrames.TalentTree:UpdateSimModeLabel() + if self.simMode then + local viewing = self.viewingClass or self.playerClass + local viewName = viewing or "?" + for _, c in ipairs(CLASS_LIST) do + if c.key == viewing then viewName = c.name; break end + end + self.frame.simModeText:SetText("|c" .. GetHex() .. "模拟: " .. viewName .. "|r") + self.frame.simLabel:SetText("|c" .. GetHex() .. "[模拟] 总点数: 51 左键: 加点 右键: 撤销|r") + self.frame.classBar:Show() + self:UpdateClassBar() + if not IsViewingOwnClass(self) then + self.frame.btnApply:Hide() + else + self.frame.btnApply:Show() + end + else + self.frame.simModeText:SetText("|cff888888模拟加点: 关|r") + self.frame.simLabel:SetText("") + self.frame.classBar:Hide() + self.frame.btnApply:Show() + if self.viewingClass and self.viewingClass ~= self.playerClass then + self.viewingClass = self.playerClass + self.virtualPoints = {} + self:DestroyTrees() + self:BuildTrees() + self:Update() + return + end + end + self:ResetVirtualPoints() +end + +-------------------------------------------------------------------------------- +-- Switch class +-------------------------------------------------------------------------------- +function SFrames.TalentTree:SwitchViewClass(classKey) + if not self.simMode then return end + if classKey == (self.viewingClass or self.playerClass) then return end + + if classKey ~= self.playerClass then + local cache = GetCache() + if not cache[classKey] then + UIErrorsFrame:AddMessage("该职业天赋数据尚未缓存,请先用该职业角色打开天赋面板", 1, 0.5, 0.5, 1) + return + end + end + + self.viewingClass = classKey + self.virtualPoints = {} + self:DestroyTrees() + self:BuildTrees() + self:UpdateSimModeLabel() + self:Update() +end + +-------------------------------------------------------------------------------- +-- Destroy / Rebuild trees +-------------------------------------------------------------------------------- +function SFrames.TalentTree:DestroyTrees() + if self.tabs then + for _, tabData in pairs(self.tabs) do + if type(tabData) == "table" and tabData.frame then + tabData.frame:Hide() + tabData.frame:ClearAllPoints() + tabData.frame:SetParent(nil) + end + end + end + self.tabs = {} + self.treesBuilt = false +end + +-------------------------------------------------------------------------------- +-- Build trees (from API or cache) +-------------------------------------------------------------------------------- +function SFrames.TalentTree:BuildTrees() + if self.treesBuilt then return end + self.treesBuilt = true + + if IsViewingOwnClass(self) then + CacheCurrentClassData() + end + + local treeTop = -38 + if self.simMode then treeTop = -52 end + + local numTabs = TT_GetNumTabs(self) + for t = 1, numTabs do + local name, icon, pointsSpent, background = TT_GetTabInfo(self, t) + + local tabFrame = CreateFrame("Frame", nil, self.frame) + tabFrame:SetWidth(TAB_WIDTH) + tabFrame:SetHeight(FRAME_HEIGHT - 90) + local offsetX = 10 + ((t - 1) * (TAB_WIDTH + 5)) + tabFrame:SetPoint("TOPLEFT", self.frame, "TOPLEFT", offsetX, treeTop) + + SetRoundBackdrop(tabFrame, T.tabBg, T.tabBorder) + + if background and background ~= "" then + local bg = tabFrame:CreateTexture(nil, "BACKGROUND") + bg:SetTexture("Interface\\TalentFrame\\" .. background) + bg:SetPoint("TOPLEFT", tabFrame, "TOPLEFT", 3, -3) + bg:SetPoint("BOTTOMRIGHT", tabFrame, "BOTTOMRIGHT", -3, 3) + bg:SetAlpha(0.35) + end + + local tTitle = MakeFS(tabFrame, 14, "CENTER", T.titleColor) + tTitle:SetPoint("TOP", tabFrame, "TOP", 0, -10) + tTitle:SetText("|c" .. GetHex() .. (name or "") .. "|r") + + local tPoints = MakeFS(tabFrame, 12, "CENTER", T.dimText) + tPoints:SetPoint("TOP", tTitle, "BOTTOM", 0, -2) + tabFrame.pointsText = tPoints + + self.tabs[t] = { frame = tabFrame, talents = {}, grid = {} } + + local GRID_PAD_X = math.floor((TAB_WIDTH - (4 * ICON_SIZE + 3 * ICON_SPACING_X)) / 2) + + local numTalents = TT_GetNumTalents(self, t) + for i = 1, numTalents do + local tName, tIcon, tier, column, rank, maxRank = TT_GetTalentInfo(self, t, i) + if not tName then break end + + if not self.tabs[t].grid[tier] then self.tabs[t].grid[tier] = {} end + + local btn = CreateFrame("Button", "SFramesTalent_" .. (self.viewingClass or "own") .. "_" .. t .. "_" .. i, tabFrame) + btn:SetWidth(ICON_SIZE) + btn:SetHeight(ICON_SIZE) + + local x = (column - 1) * (ICON_SIZE + ICON_SPACING_X) + GRID_PAD_X + local y = -(tier - 1) * (ICON_SIZE + ICON_SPACING_Y) - 44 + btn:SetPoint("TOPLEFT", tabFrame, "TOPLEFT", x, y) + + btn.icon = btn:CreateTexture(nil, "ARTWORK") + btn.icon:SetTexture(tIcon) + btn.icon:SetAllPoints() + + btn.borderTex = btn:CreateTexture(nil, "OVERLAY") + btn.borderTex:SetTexture("Interface\\Buttons\\UI-Quickslot2") + btn.borderTex:SetWidth(ICON_SIZE * 1.5) + btn.borderTex:SetHeight(ICON_SIZE * 1.5) + btn.borderTex:SetPoint("CENTER", btn, "CENTER", 0, 0) + + local rankFrame = CreateFrame("Frame", nil, btn) + rankFrame:SetAllPoints(btn) + rankFrame:SetFrameLevel(btn:GetFrameLevel() + 2) + + btn.rankBg = rankFrame:CreateTexture(nil, "BACKGROUND") + btn.rankBg:SetTexture(0, 0, 0, 0.75) + btn.rankBg:SetWidth(20) + btn.rankBg:SetHeight(12) + btn.rankBg:SetPoint("BOTTOMRIGHT", rankFrame, "BOTTOMRIGHT", -1, 1) + btn.rankBg:Hide() + + btn.rankText = MakeFS(rankFrame, 10, "RIGHT") + btn.rankText:SetPoint("BOTTOMRIGHT", rankFrame, "BOTTOMRIGHT", -2, 2) + btn.rankText:SetShadowColor(0, 0, 0, 1) + btn.rankText:SetShadowOffset(1, -1) + btn.rankText:SetText("") + + btn.tab = t + btn.index = i + btn.tier = tier + btn.maxRank = maxRank + btn.talentName = tName + + local cachedDesc = nil + if not IsViewingOwnClass(self) then + local cd = GetCache()[self.viewingClass] + if cd and cd[t] and cd[t].talents[i] then + cachedDesc = cd[t].talents[i].desc + end + end + btn.cachedDesc = cachedDesc + + if IsViewingOwnClass(self) then + btn:SetScript("OnEnter", function() + GameTooltip_SetDefaultAnchor(GameTooltip, this) + GameTooltip:SetTalent(this.tab, this.index) + GameTooltip:Show() + end) + else + btn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:AddLine(this.talentName or "?", 1, 1, 1) + local vr = SFrames.TalentTree:GetVirtualRank(this.tab, this.index) + GameTooltip:AddLine("等级 " .. vr .. "/" .. (this.maxRank or "?"), 0.7, 0.7, 0.7) + if this.cachedDesc then + GameTooltip:AddLine(" ") + for _, line in ipairs(this.cachedDesc) do + GameTooltip:AddLine(line, 1, 0.82, 0, 1) + end + end + GameTooltip:Show() + end) + end + btn:SetScript("OnLeave", function() GameTooltip:Hide() end) + + btn:RegisterForClicks("LeftButtonUp", "RightButtonUp") + btn:SetScript("OnClick", function() + SFrames.TalentTree:OnTalentClick(this, arg1) + end) + + self.tabs[t].talents[i] = btn + self.tabs[t].grid[tier][column] = btn + end + + -- Dependency lines + for i = 1, numTalents do + local prereqTier, prereqColumn = TT_GetTalentPrereqs(self, t, i) + if prereqTier and prereqColumn then + local btn = self.tabs[t].talents[i] + local pBtn = self.tabs[t].grid[prereqTier] and self.tabs[t].grid[prereqTier][prereqColumn] + if pBtn then + btn.prereqIndex = pBtn.index + btn.prereqLines = {} + + local _, _, tier, column = TT_GetTalentInfo(self, t, i) + local pX = (prereqColumn - 1) * (ICON_SIZE + ICON_SPACING_X) + GRID_PAD_X + local pY = -(prereqTier - 1) * (ICON_SIZE + ICON_SPACING_Y) - 44 + local cX = (column - 1) * (ICON_SIZE + ICON_SPACING_X) + GRID_PAD_X + local cY = -(tier - 1) * (ICON_SIZE + ICON_SPACING_Y) - 44 + + local pCenterX = pX + (ICON_SIZE / 2) + local pCenterY = pY - (ICON_SIZE / 2) + local pBottomY = pY - ICON_SIZE + local pRightX = pX + ICON_SIZE + + local tCenterX = cX + (ICON_SIZE / 2) + local tTopY = cY + local tLeftX = cX + local tRightX = cX + ICON_SIZE + + local function CreateLine(x1, y1, x2, y2) + local line = tabFrame:CreateTexture(nil, "BACKGROUND") + line:SetTexture(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + if math.abs(x1 - x2) < 0.1 then + line:SetWidth(2) + line:SetHeight(math.abs(y1 - y2)) + line:SetPoint("TOP", tabFrame, "TOPLEFT", x1, math.max(y1, y2)) + else + line:SetHeight(2) + line:SetWidth(math.abs(x1 - x2)) + line:SetPoint("LEFT", tabFrame, "TOPLEFT", math.min(x1, x2), y1) + end + table.insert(btn.prereqLines, line) + end + + if prereqTier == tier then + if prereqColumn < column then + CreateLine(pRightX, pCenterY, tLeftX, pCenterY) + else + CreateLine(pX, pCenterY, tRightX, pCenterY) + end + elseif prereqColumn == column then + CreateLine(pCenterX, pBottomY, tCenterX, tTopY) + else + local midY = pBottomY - (ICON_SPACING_Y / 2) + 2 + CreateLine(pCenterX, pBottomY, pCenterX, midY) + CreateLine(pCenterX, midY, tCenterX, midY) + CreateLine(tCenterX, midY, tCenterX, tTopY) + end + end + end + end + end +end + +-------------------------------------------------------------------------------- +-- Hooks +-------------------------------------------------------------------------------- +function SFrames.TalentTree:HookVanillaUI() + local self = SFrames.TalentTree + + local function ShowCustomTalentFrame() + if not self.frame then return end + if not self.frame:IsShown() then + self:BuildTrees() + self.frame:Show() + self:Update() + end + end + + local function ToggleCustomTalentFrame() + if not self.frame then return end + if self.frame:IsShown() then + self.frame:Hide() + else + ShowCustomTalentFrame() + end + end + + ToggleTalentFrame = ToggleCustomTalentFrame + + if not self.hookedShowUIPanel then + self.hookedShowUIPanel = true + local orig_ShowUIPanel = ShowUIPanel + ShowUIPanel = function(frame) + if frame and frame.GetName then + local fName = frame:GetName() + if fName == "TalentFrame" or fName == "TalentMicroButtonAlert" then + ShowCustomTalentFrame() + return + end + end + if orig_ShowUIPanel then + orig_ShowUIPanel(frame) + end + end + + local orig_HideUIPanel = HideUIPanel + HideUIPanel = function(frame) + if frame and frame.GetName and frame:GetName() == "TalentFrame" then + if self.frame then self.frame:Hide() end + return + end + if orig_HideUIPanel then + orig_HideUIPanel(frame) + end + end + end + + local orig_TalentFrame_LoadUI = TalentFrame_LoadUI + if orig_TalentFrame_LoadUI then + TalentFrame_LoadUI = function() + orig_TalentFrame_LoadUI() + end + end +end + +-------------------------------------------------------------------------------- +-- Virtual point helpers +-------------------------------------------------------------------------------- +function SFrames.TalentTree:GetVirtualRank(tab, index) + if self.virtualPoints[tab] and self.virtualPoints[tab][index] then + return self.virtualPoints[tab][index] + end + if self.simMode then return 0 end + if IsViewingOwnClass(self) then + local name, icon, tier, column, rank, maxRank = GetTalentInfo(tab, index) + return rank or 0 + end + return 0 +end + +function SFrames.TalentTree:GetVirtualTreePoints(tab) + local total = 0 + local n = TT_GetNumTalents(self, tab) + for i = 1, n do + total = total + self:GetVirtualRank(tab, i) + end + return total +end + +function SFrames.TalentTree:GetRemainingUnspent() + if self.simMode then + local unspent = 51 + local numTabs = TT_GetNumTabs(self) + for tb = 1, numTabs do + unspent = unspent - self:GetVirtualTreePoints(tb) + end + return unspent + else + local unspent = UnitCharacterPoints("player") + for tb = 1, GetNumTalentTabs() do + for idx = 1, GetNumTalents(tb) do + local name, icon, tier, column, realRank = GetTalentInfo(tb, idx) + local virtRank = self:GetVirtualRank(tb, idx) + unspent = unspent - (virtRank - (realRank or 0)) + end + end + return unspent + end +end + +-------------------------------------------------------------------------------- +-- Talent click +-------------------------------------------------------------------------------- +function SFrames.TalentTree:OnTalentClick(btn, buttonType) + if not self.simMode and IsViewingOwnClass(self) then + if buttonType == "LeftButton" then + local tName, tIcon, tier, column, rank, maxRank, isExceptional, meetsPrereq = GetTalentInfo(btn.tab, btn.index) + local unspent = UnitCharacterPoints("player") + if rank < maxRank and meetsPrereq and unspent > 0 then + StaticPopupDialogs["NANAMI_CONFIRM_TALENT"] = { + text = "确定学习天赋 [" .. tName .. "] ?\n(等级 " .. rank .. " → " .. (rank+1) .. " / " .. maxRank .. ")", + button1 = "确认", + button2 = "取消", + OnAccept = function() + LearnTalent(btn.tab, btn.index) + end, + timeout = 0, + whileDead = true, + hideOnEscape = true, + } + StaticPopup_Show("NANAMI_CONFIRM_TALENT") + end + end + return + end + + if not self.simMode then return end + + local _, _, tier, _, _, maxRank = TT_GetTalentInfo(self, btn.tab, btn.index) + local virtRank = self:GetVirtualRank(btn.tab, btn.index) + maxRank = maxRank or btn.maxRank + + if not self.virtualPoints[btn.tab] then self.virtualPoints[btn.tab] = {} end + if not self.virtualPoints[btn.tab][btn.index] then + self.virtualPoints[btn.tab][btn.index] = self:GetVirtualRank(btn.tab, btn.index) + end + + if buttonType == "LeftButton" then + local unspent = self:GetRemainingUnspent() + if unspent > 0 and virtRank < maxRank then + local treePts = self:GetVirtualTreePoints(btn.tab) + local prereqMet = true + if btn.prereqIndex then + local pVirtRank = self:GetVirtualRank(btn.tab, btn.prereqIndex) + local pMaxRank = self.tabs[btn.tab].talents[btn.prereqIndex].maxRank + if pVirtRank < pMaxRank then prereqMet = false end + end + + if treePts >= (tier - 1) * 5 and prereqMet then + self.virtualPoints[btn.tab][btn.index] = virtRank + 1 + end + end + elseif buttonType == "RightButton" then + if virtRank > 0 then + local canRevert = true + local treePts = self:GetVirtualTreePoints(btn.tab) + local numT = TT_GetNumTalents(self, btn.tab) + for i = 1, numT do + local childVirtRank = self:GetVirtualRank(btn.tab, i) + if childVirtRank > 0 then + local _, _, qTier = TT_GetTalentInfo(self, btn.tab, i) + if qTier and qTier > tier and (treePts - 1) < (qTier - 1) * 5 then + canRevert = false + end + local cBtn = self.tabs[btn.tab].talents[i] + if cBtn and cBtn.prereqIndex == btn.index then + if childVirtRank > 0 and (virtRank - 1) < maxRank then + canRevert = false + end + end + end + end + if canRevert then + self.virtualPoints[btn.tab][btn.index] = virtRank - 1 + else + UIErrorsFrame:AddMessage("无法取消点数:其他已点天赋依赖于此天赋", 1, 0, 0, 1) + end + end + end + + self:Update() +end + +function SFrames.TalentTree:ResetVirtualPoints() + self.virtualPoints = {} + self:Update() +end + +function SFrames.TalentTree:ApplyVirtualPoints() + if not self.simMode then return end + if not IsViewingOwnClass(self) then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 只能应用本职业的天赋。") + return + end + + local canApply = true + for tb = 1, GetNumTalentTabs() do + for idx = 1, GetNumTalents(tb) do + local name, icon, tier, column, realRank = GetTalentInfo(tb, idx) + local virtRank = self:GetVirtualRank(tb, idx) + if realRank > virtRank then + canApply = false + end + end + end + + if not canApply then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 模拟的点数未包含您已学习的天赋,无法直接应用。") + return + end + + if not self.applyQueue then self.applyQueue = {} end + self.applyQueue = {} + + for tb = 1, GetNumTalentTabs() do + for idx = 1, GetNumTalents(tb) do + local name, icon, tier, column, realRank = GetTalentInfo(tb, idx) + local virtRank = self:GetVirtualRank(tb, idx) + local diff = virtRank - realRank + if diff > 0 then + for i = 1, diff do + table.insert(self.applyQueue, {tab = tb, index = idx}) + end + end + end + end + + if table.getn(self.applyQueue) > 0 then + self.frame:SetScript("OnUpdate", function() + if table.getn(SFrames.TalentTree.applyQueue) > 0 then + local t = table.remove(SFrames.TalentTree.applyQueue, 1) + LearnTalent(t.tab, t.index) + else + this:SetScript("OnUpdate", nil) + SFrames.TalentTree.simMode = false + SFrames.TalentTree:UpdateSimModeLabel() + end + end) + else + DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 没有新的天赋点数需要应用。") + end +end + +-------------------------------------------------------------------------------- +-- Update display +-------------------------------------------------------------------------------- +function SFrames.TalentTree:Update() + if not self.frame or not self.frame:IsShown() then return end + if not self.tabs or not self.tabs[1] then return end + + local unspent = self:GetRemainingUnspent() + local realUnspent = UnitCharacterPoints("player") + local numTabs = TT_GetNumTabs(self) + + for tb = 1, numTabs do + if not self.tabs[tb] then break end + local treePts = self:GetVirtualTreePoints(tb) + self.tabs[tb].frame.pointsText:SetText("已用: |c" .. GetHex() .. treePts .. "|r") + + local numT = TT_GetNumTalents(self, tb) + for idx = 1, numT do + local btn = self.tabs[tb].talents[idx] + if btn then + local _, _, tier, column, rank, maxRank = TT_GetTalentInfo(self, tb, idx) + if not IsViewingOwnClass(self) then rank = 0 end + local virtRank = self:GetVirtualRank(tb, idx) + + if virtRank > 0 or (not self.simMode and rank and rank > 0) then + local displayRank = virtRank + if not self.simMode and displayRank == 0 and rank and rank > 0 then + displayRank = rank + end + btn.rankText:SetText(displayRank .. "/" .. maxRank) + btn.rankBg:Show() + btn.rankText:Show() + else + btn.rankText:SetText("") + btn.rankText:Hide() + btn.rankBg:Hide() + end + + local prereqMet = true + if btn.prereqIndex then + local pVirtRank = self:GetVirtualRank(tb, btn.prereqIndex) + local pMaxRank = self.tabs[tb].talents[btn.prereqIndex].maxRank + if pVirtRank < pMaxRank then + prereqMet = false + end + end + + local isLearnable = (unspent > 0 and treePts >= (tier - 1) * 5 and virtRank < maxRank and prereqMet) + + if btn.prereqLines then + for _, line in ipairs(btn.prereqLines) do + if prereqMet then + line:SetTexture(T.accent[1], T.accent[2], T.accent[3], 0.8) + else + line:SetTexture(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + end + end + end + + local isVirtual = (not self.simMode and virtRank > (rank or 0)) or (self.simMode and virtRank > 0) + + if (not self.simMode and isVirtual) or (self.simMode and isVirtual and virtRank < maxRank) then + btn.rankText:SetTextColor(T.title[1], T.title[2], T.title[3]) + btn.icon:SetVertexColor(T.accentLight[1], T.accentLight[2], T.accentLight[3]) + btn.borderTex:SetVertexColor(T.accent[1], T.accent[2], T.accent[3]) + elseif virtRank >= maxRank then + btn.rankText:SetTextColor(T.title[1], T.title[2], T.title[3]) + btn.icon:SetVertexColor(1, 1, 1) + btn.borderTex:SetVertexColor(T.accent[1], T.accent[2], T.accent[3]) + elseif virtRank > 0 then + btn.rankText:SetTextColor(0, 1, 0) + btn.icon:SetVertexColor(1, 1, 1) + btn.borderTex:SetVertexColor(0, 0.7, 0) + elseif isLearnable then + btn.rankText:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + btn.icon:SetVertexColor(1, 1, 1) + btn.borderTex:SetVertexColor(T.passive[1], T.passive[2], T.passive[3]) + else + btn.rankText:SetTextColor(T.passive[1], T.passive[2], T.passive[3]) + btn.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3]) + btn.borderTex:SetVertexColor(T.passive[1], T.passive[2], T.passive[3]) + end + end + end + end + + if self.simMode then + if IsViewingOwnClass(self) then + self.frame.pointsText:SetText("剩余天赋点: |c" .. GetHex() .. unspent .. " |cff888888(总 " .. realUnspent .. ")|r") + else + self.frame.pointsText:SetText("剩余天赋点: |c" .. GetHex() .. unspent .. "|r") + end + else + self.frame.pointsText:SetText("剩余天赋点: |c" .. GetHex() .. realUnspent .. "|r") + end +end diff --git a/Units/Target.lua b/Units/Target.lua new file mode 100644 index 0000000..966ba88 --- /dev/null +++ b/Units/Target.lua @@ -0,0 +1,1046 @@ +SFrames.Target = {} +local _A = SFrames.ActiveTheme + +local function Clamp(value, minValue, maxValue) + if value < minValue then + return minValue + end + if value > maxValue then + return maxValue + end + return value +end + +function SFrames.Target:GetDistance(unit) + if not UnitExists(unit) then return nil end + if UnitIsUnit(unit, "player") then return "0 码" end + -- Using multiple "scale rulers" (rungs) for better precision in 1.12 + if CheckInteractDistance(unit, 2) then return "< 8 码" -- Trade + elseif CheckInteractDistance(unit, 3) then return "8-10 码" -- Duel + elseif CheckInteractDistance(unit, 4) then return "10-28 码" -- Follow + elseif UnitIsVisible(unit) then return "28-100 码" + else return "> 100 码" end +end + +function SFrames.Target:GetConfig() + local db = SFramesDB or {} + + local width = tonumber(db.targetFrameWidth) or SFrames.Config.width or 220 + width = Clamp(math.floor(width + 0.5), 170, 420) + + local portraitWidth = tonumber(db.targetPortraitWidth) or SFrames.Config.portraitWidth or 50 + portraitWidth = Clamp(math.floor(portraitWidth + 0.5), 32, 95) + if portraitWidth > width - 90 then + portraitWidth = width - 90 + end + + local healthHeight = tonumber(db.targetHealthHeight) or 38 + healthHeight = Clamp(math.floor(healthHeight + 0.5), 14, 80) + + local powerHeight = tonumber(db.targetPowerHeight) or 9 + powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, 40) + + local height = healthHeight + powerHeight + 4 + height = Clamp(height, 30, 140) + + local nameFont = tonumber(db.targetNameFontSize) or 10 + nameFont = Clamp(math.floor(nameFont + 0.5), 8, 18) + + local valueFont = tonumber(db.targetValueFontSize) or 10 + valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18) + + local frameScale = tonumber(db.targetFrameScale) or 1 + frameScale = Clamp(frameScale, 0.7, 1.8) + + return { + width = width, + height = height, + portraitWidth = portraitWidth, + healthHeight = healthHeight, + powerHeight = powerHeight, + nameFont = nameFont, + valueFont = valueFont, + scale = frameScale, + } +end + +function SFrames.Target:ApplyConfig() + if not self.frame then return end + + local cfg = self:GetConfig() + local f = self.frame + + f:SetScale(cfg.scale) + f:SetWidth(cfg.width) + f:SetHeight(cfg.height) + + if f.portrait then + f.portrait:SetWidth(cfg.portraitWidth) + f.portrait:SetHeight(cfg.height - 2) + end + + if f.portraitBG then + f.portraitBG:ClearAllPoints() + f.portraitBG:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -1, 0) + f.portraitBG:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) + end + + if f.health then + f.health:ClearAllPoints() + f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) + f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0) + f.health:SetHeight(cfg.healthHeight) + end + + if f.healthBGFrame then + f.healthBGFrame:ClearAllPoints() + f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1) + f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) + end + + if f.power then + f.power:ClearAllPoints() + f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) + f.power:SetPoint("TOPRIGHT", f.health, "BOTTOMRIGHT", 0, 0) + f.power:SetHeight(cfg.powerHeight) + end + + if f.powerBGFrame then + f.powerBGFrame:ClearAllPoints() + f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1) + f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) + end + + local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" + local fontPath = SFrames:GetFont() + + if f.nameText then + f.nameText:SetFont(fontPath, cfg.nameFont, outline) + end + if f.healthText then + f.healthText:SetFont(fontPath, cfg.valueFont, outline) + end + if f.powerText then + f.powerText:SetFont(fontPath, cfg.valueFont, outline) + end + + if self.distanceFrame then + local dScale = tonumber(SFramesDB and SFramesDB.targetDistanceScale) or 1 + self.distanceFrame:SetScale(Clamp(dScale, 0.7, 1.8)) + end + + if UnitExists("target") then + self:UpdateAll() + end +end + +function SFrames.Target:InitializeDistanceFrame() + local f = CreateFrame("Button", "SFramesTargetDistanceFrame", UIParent) + f:SetWidth(80) + f:SetHeight(24) + f:SetFrameStrata("HIGH") + local frameScale = (SFramesDB and type(SFramesDB.targetDistanceScale) == "number") and SFramesDB.targetDistanceScale or 1 + f:SetScale(frameScale) + + if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetDistanceFrame"] then + local pos = SFramesDB.Positions["TargetDistanceFrame"] + f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs) + else + -- Default position: Center of screen for visibility if first time + f:SetPoint("CENTER", UIParent, "CENTER", 0, 100) + end + + f:SetMovable(true) + f:EnableMouse(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() if IsAltKeyDown() or SFrames.isUnlocked then this:StartMoving() end end) + f:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + if not SFramesDB then SFramesDB = {} end + if not SFramesDB.Positions then SFramesDB.Positions = {} end + local point, relativeTo, relativePoint, xOfs, yOfs = this:GetPoint() + SFramesDB.Positions["TargetDistanceFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs } + end) + + SFrames:CreateUnitBackdrop(f) + f:SetBackdrop(nil) -- Remove border and background for natural look + + f.text = SFrames:CreateFontString(f, 14, "CENTER") + f.text:SetPoint("CENTER", f, "CENTER", 0, 0) + f.text:SetTextColor(1, 0.8, 0.2) + f.text:SetShadowColor(0, 0, 0, 1) + f.text:SetShadowOffset(1, -1) + + SFrames.Target.distanceFrame = f + f:Hide() + + -- Distance Updater on the frame itself + f.timer = 0 + f:SetScript("OnUpdate", function() + if SFramesDB and SFramesDB.targetDistanceEnabled == false then + if this:IsShown() then this:Hide() end + return + end + if not UnitExists("target") then + if this:IsShown() then this:Hide() end + return + end + this.timer = this.timer + (arg1 or 0) + if this.timer >= 0.4 then + this.timer = 0 + local dist = SFrames.Target:GetDistance("target") + this.text:SetText(dist or "---") + if not this:IsShown() then this:Show() end + end + end) +end + +local AURA_SIZE = 24 +local AURA_SPACING = 2 +local AURA_ROW_SPACING = 1 + +local function GetIncomingHeals(unit) + return SFrames:GetIncomingHeals(unit) +end + +local function TryDropCursorOnUnit(unit) + if not unit or not UnitExists(unit) then return false end + if not CursorHasItem or not CursorHasItem() then return false end + if not DropItemOnUnit then return false end + + local ok = pcall(DropItemOnUnit, unit) + if not ok then + return false + end + + return not CursorHasItem() +end + +function SFrames.Target:Initialize() + local f = CreateFrame("Button", "SFramesTargetFrame", UIParent) + f:SetWidth(SFrames.Config.width) + f:SetHeight(SFrames.Config.height) + if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetFrame"] then + local pos = SFramesDB.Positions["TargetFrame"] + f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs) + else + f:SetPoint("CENTER", UIParent, "CENTER", 200, -100) -- Mirrored from player + end + local frameScale = (SFramesDB and type(SFramesDB.targetFrameScale) == "number") and SFramesDB.targetFrameScale or 1 + f:SetScale(frameScale) + + f:SetMovable(true) + f:EnableMouse(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() if IsAltKeyDown() or SFrames.isUnlocked then f:StartMoving() end end) + f:SetScript("OnDragStop", function() + f:StopMovingOrSizing() + if not SFramesDB then SFramesDB = {} end + if not SFramesDB.Positions then SFramesDB.Positions = {} end + local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint() + SFramesDB.Positions["TargetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs } + end) + + f:RegisterForClicks("LeftButtonUp", "RightButtonUp") + f:SetScript("OnClick", function() + if arg1 == "LeftButton" then + if TryDropCursorOnUnit(this.unit) then + return + end + if SpellIsTargeting and SpellIsTargeting() then + SpellTargetUnit(this.unit) + end + elseif arg1 == "RightButton" then + if SpellIsTargeting() then + SpellStopTargeting() + return + end + HideDropDownMenu(1) + TargetFrameDropDown.unit = "target" + TargetFrameDropDown.name = UnitName("target") + TargetFrameDropDown.initialize = TargetFrameDropDown_Initialize + ToggleDropDownMenu(1, nil, TargetFrameDropDown, "SFramesTargetFrame", 120, 10) + end + end) + f:SetScript("OnReceiveDrag", function() + if TryDropCursorOnUnit(this.unit) then + return + end + if SpellIsTargeting and SpellIsTargeting() then + SpellTargetUnit(this.unit) + end + end) + + SFrames:CreateUnitBackdrop(f) + + -- 3D Portrait (Right side for target) + local pWidth = SFrames.Config.portraitWidth + f.portrait = CreateFrame("PlayerModel", nil, f) + f.portrait:SetWidth(pWidth) + f.portrait:SetHeight(SFrames.Config.height - 2) + f.portrait:SetPoint("RIGHT", f, "RIGHT", -1, 0) + + local pbg = CreateFrame("Frame", nil, f) + pbg:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -1, 0) + pbg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) + pbg:SetFrameLevel(f:GetFrameLevel()) + SFrames:CreateUnitBackdrop(pbg) + f.portraitBG = pbg + + -- Health Bar (Left side) + f.health = SFrames:CreateStatusBar(f, "SFramesTargetHealth") + f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) + f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0) + f.health:SetHeight((SFrames.Config.height - 2) * 0.82 - 1) + + local hbg = CreateFrame("Frame", nil, f) + hbg:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1) + hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) + hbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(hbg) + f.healthBGFrame = hbg + + -- Add a dark backdrop behind the health texture + f.health.bg = f.health:CreateTexture(nil, "BACKGROUND") + f.health.bg:SetAllPoints() + f.health.bg:SetTexture(SFrames:GetTexture()) + f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + -- Heal prediction overlay (incoming heals) + f.health.healPredMine = f.health:CreateTexture(nil, "OVERLAY") + f.health.healPredMine:SetTexture(SFrames:GetTexture()) + f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78) + f.health.healPredMine:Hide() + + f.health.healPredOther = f.health:CreateTexture(nil, "OVERLAY") + f.health.healPredOther:SetTexture(SFrames:GetTexture()) + f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5) + f.health.healPredOther:Hide() + + -- Power Bar + f.power = SFrames:CreateStatusBar(f, "SFramesTargetPower") + f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1) + f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0) + + local powerbg = CreateFrame("Frame", nil, f) + powerbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1) + powerbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) + powerbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(powerbg) + f.powerBGFrame = powerbg + + -- Add a dark backdrop behind the power texture + f.power.bg = f.power:CreateTexture(nil, "BACKGROUND") + f.power.bg:SetAllPoints() + f.power.bg:SetTexture(SFrames:GetTexture()) + f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + -- Class Icon Badge (overlaid on portrait, top-right corner with 1/3 outside) + f.classIcon = SFrames:CreateClassIcon(f, 16) + f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0) + + -- Texts + f.nameText = SFrames:CreateFontString(f.health, 10, "LEFT") + f.nameText:SetPoint("LEFT", f.health, "LEFT", 4, 0) + + f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT") + f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0) + + f.powerText = SFrames:CreateFontString(f.power, 10, "RIGHT") + f.powerText:SetPoint("RIGHT", f.power, "RIGHT", -4, 0) + + -- Outline/shadow setup for text to make it pop + f.nameText:SetShadowColor(0, 0, 0, 1) + f.nameText:SetShadowOffset(1, -1) + f.healthText:SetShadowColor(0, 0, 0, 1) + f.healthText:SetShadowOffset(1, -1) + f.powerText:SetShadowColor(0, 0, 0, 1) + f.powerText:SetShadowOffset(1, -1) + + -- Combo Points + f.comboText = SFrames:CreateFontString(f, 20, "CENTER") + f.comboText:SetPoint("CENTER", f.portrait, "CENTER", 0, 0) + f.comboText:SetTextColor(1, 0.8, 0) + f.comboText:SetText("") + + -- Raid Target Icon (top center of health bar, half outside frame) + local raidIconSize = 22 + local raidIconOvr = CreateFrame("Frame", nil, f) + raidIconOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 5) + raidIconOvr:SetWidth(raidIconSize) + raidIconOvr:SetHeight(raidIconSize) + raidIconOvr:SetPoint("CENTER", f.health, "TOP", 0, 0) + f.raidIcon = raidIconOvr:CreateTexture(nil, "OVERLAY") + f.raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons") + f.raidIcon:SetAllPoints(raidIconOvr) + f.raidIcon:Hide() + f.raidIconOverlay = raidIconOvr + + self.frame = f + self:ApplyConfig() + f:Hide() + + SFrames:RegisterEvent("PLAYER_TARGET_CHANGED", function() self:OnTargetChanged() end) + SFrames:RegisterEvent("UNIT_HEALTH", function() if arg1 == "target" then self:UpdateHealth() end end) + SFrames:RegisterEvent("UNIT_MAXHEALTH", function() if arg1 == "target" then self:UpdateHealth() end end) + SFrames:RegisterEvent("UNIT_MANA", function() if arg1 == "target" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_MAXMANA", function() if arg1 == "target" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_ENERGY", function() if arg1 == "target" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_MAXENERGY", function() if arg1 == "target" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_RAGE", function() if arg1 == "target" then self:UpdatePower() end end) + SFrames:RegisterEvent("UNIT_MAXRAGE", function() if arg1 == "target" then self:UpdatePower() end end) + SFrames:RegisterEvent("PLAYER_COMBO_POINTS", function() self:UpdateComboPoints() end) + SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if arg1 == "target" then self:UpdatePowerType() end end) + SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function() if arg1 == "target" then self.frame.portrait:SetUnit("target") self.frame.portrait:SetCamera(0) self.frame.portrait:SetPosition(-1.0, 0, 0) end end) + SFrames:RegisterEvent("UNIT_DYNAMIC_FLAGS", function() if arg1 == "target" then self:UpdateAll() end end) + SFrames:RegisterEvent("UNIT_FACTION", function() if arg1 == "target" then self:UpdateAll() end end) + SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcon() end) + + self:CreateAuras() + self:CreateCastbar() + self:InitializeDistanceFrame() + + f.unit = "target" + f:SetScript("OnEnter", function() + GameTooltip_SetDefaultAnchor(GameTooltip, this) + GameTooltip:SetUnit(this.unit) + GameTooltip:Show() + end) + f:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + -- If target already exists on load (e.g. after /reload), show and update it immediately + self:OnTargetChanged() + + -- Distance Updater removed from target frame +end + +function SFrames.Target:OnTargetChanged() + if UnitExists("target") then + self.frame:Show() + self:UpdateAll() + -- Force distance update immediately + if SFrames.Target.distanceFrame then + local dist = self:GetDistance("target") + SFrames.Target.distanceFrame.text:SetText(dist or "---") + if not (SFramesDB and SFramesDB.targetDistanceEnabled == false) then + SFrames.Target.distanceFrame:Show() + else + SFrames.Target.distanceFrame:Hide() + end + end + else + self.frame:Hide() + if SFrames.Target.distanceFrame then SFrames.Target.distanceFrame:Hide() end + end +end + +function SFrames.Target:UpdateAll() + self:UpdateHealth() + self:UpdatePowerType() + self:UpdatePower() + self:UpdateComboPoints() + self:UpdateRaidIcon() + self:UpdateAuras() + + self.frame.portrait:SetUnit("target") + self.frame.portrait:SetCamera(0) + self.frame.portrait:Hide() + self.frame.portrait:Show() + self.frame.portrait:SetPosition(-1.0, 0, 0) + + local name = UnitName("target") or "" + local level = UnitLevel("target") + local levelText = level + + -- Difficulty Color logic + local function RGBToHex(r, g, b) + return string.format("|cff%02x%02x%02x", r*255, g*255, b*255) + end + + local function GetLevelDiffColor(targetLevel) + local playerLevel = UnitLevel("player") + if targetLevel == -1 then return 1, 0, 0 end -- Skull + + local diff = targetLevel - playerLevel + if diff >= 5 then + return 1, 0.1, 0.1 -- Red + elseif diff >= 3 then + return 1, 0.5, 0.25 -- Orange + elseif diff >= -2 then + return 1, 1, 0 -- Yellow + elseif -diff <= GetQuestGreenRange() then + return 0.25, 0.75, 0.25 -- Green + else + return 0.5, 0.5, 0.5 -- Grey + end + end + + local levelColor = RGBToHex(1, 1, 1) -- default white + + if level == -1 then + levelText = "??" + levelColor = RGBToHex(1, 0, 0) -- skull is always red + else + local r, g, b = GetLevelDiffColor(level) + levelColor = RGBToHex(r, g, b) + end + + local classif = UnitClassification("target") + if classif == "elite" or classif == "rareelite" then + levelText = levelText .. "+" + elseif classif == "rare" then + levelText = levelText .. "R" + elseif classif == "worldboss" then + levelText = "??" + levelColor = RGBToHex(1, 0, 0) + end + + local formattedLevel = levelColor .. levelText .. "|r" + + -- Toggle level display from config DB + if SFramesDB and SFramesDB.showLevel == false then + formattedLevel = "" + else + formattedLevel = formattedLevel .. " " + end + local showClassText = not (SFramesDB and SFramesDB.targetShowClass == false) + if showClassText and UnitIsPlayer("target") then + local localizedClass = UnitClass("target") + if localizedClass and localizedClass ~= "" then + name = name .. " " .. localizedClass + end + end + + if UnitIsPlayer("target") and not (SFramesDB and SFramesDB.targetShowClassIcon == false) then + local _, tClass = UnitClass("target") + SFrames:SetClassIcon(self.frame.classIcon, tClass) + else + if self.frame.classIcon then + self.frame.classIcon:Hide() + if self.frame.classIcon.overlay then self.frame.classIcon.overlay:Hide() end + end + end + + local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false) + + if UnitIsPlayer("target") and useClassColor then + local _, class = UnitClass("target") + if class and SFrames.Config.colors.class[class] then + local color = SFrames.Config.colors.class[class] + -- Set Health Color + self.frame.health:SetStatusBarColor(color.r, color.g, color.b) + -- Apply Class Color to Name + self.frame.nameText:SetText(formattedLevel .. name) + self.frame.nameText:SetTextColor(color.r, color.g, color.b) + else + self.frame.health:SetStatusBarColor(0, 1, 0) + self.frame.nameText:SetText(formattedLevel .. name) + self.frame.nameText:SetTextColor(1, 1, 1) + end + else + local r, g, b = 0.85, 0.77, 0.36 -- Neutral (Softer Yellow) + local isTapped = UnitIsTapped("target") and not UnitIsTappedByPlayer("target") + if isTapped then + r, g, b = 0.53, 0.53, 0.53 -- Tapped by others (Grey) + name = name .. " (无拾取)" -- For colorblind + elseif UnitIsEnemy("player", "target") then + r, g, b = 0.78, 0.25, 0.25 -- Enemy (Softer Red) + elseif UnitIsFriend("player", "target") then + r, g, b = 0.33, 0.59, 0.33 -- Friend (Softer Green) + end + self.frame.health:SetStatusBarColor(r, g, b) + -- Color Name same as reaction + self.frame.nameText:SetText(formattedLevel .. name) + self.frame.nameText:SetTextColor(r, g, b) + end +end + +function SFrames.Target:UpdateHealth() + local hp = UnitHealth("target") + local maxHp = UnitHealthMax("target") + self.frame.health:SetMinMaxValues(0, maxHp) + self.frame.health:SetValue(hp) + + local displayHp, displayMax = hp, maxHp + if (not SFramesDB or SFramesDB.mobRealHealth ~= false) and maxHp == 100 + and not UnitIsPlayer("target") and not UnitPlayerControlled("target") then + local name = UnitName("target") + local level = UnitLevel("target") + if name and level and LibMobHealth_Cache then + local key = name .. ":" .. level + local realMax = LibMobHealth_Cache[key] + if realMax and realMax > 0 then + displayMax = realMax + displayHp = math.floor(realMax * hp / 100) + end + end + end + + if displayMax > 0 then + self.frame.healthText:SetText(displayHp .. " / " .. displayMax) + else + self.frame.healthText:SetText(displayHp) + end + + self:UpdateHealPrediction() +end + +function SFrames.Target:UpdateHealPrediction() + if not (self.frame and self.frame.health and self.frame.health.healPredMine and self.frame.health.healPredOther) then return end + local predMine = self.frame.health.healPredMine + local predOther = self.frame.health.healPredOther + + local function HidePredictions() + predMine:Hide() + predOther:Hide() + end + + if not UnitExists("target") then + HidePredictions() + return + end + + local hp = UnitHealth("target") or 0 + local maxHp = UnitHealthMax("target") or 0 + if maxHp <= 0 or hp >= maxHp then + HidePredictions() + return + end + + local _, mineIncoming, othersIncoming = GetIncomingHeals("target") + local missing = maxHp - hp + if missing <= 0 then + HidePredictions() + return + end + + local mineShown = math.min(math.max(0, mineIncoming), missing) + local remaining = missing - mineShown + local otherShown = math.min(math.max(0, othersIncoming), remaining) + if mineShown <= 0 and otherShown <= 0 then + HidePredictions() + return + end + + local barWidth = self.frame.health:GetWidth() or 0 + if barWidth <= 0 then + HidePredictions() + return + end + + local currentWidth = math.floor((hp / maxHp) * barWidth + 0.5) + if currentWidth < 0 then currentWidth = 0 end + if currentWidth > barWidth then currentWidth = barWidth end + + local availableWidth = barWidth - currentWidth + if availableWidth <= 0 then + HidePredictions() + return + end + + local mineWidth = math.floor((mineShown / maxHp) * barWidth + 0.5) + local otherWidth = math.floor((otherShown / maxHp) * barWidth + 0.5) + if mineWidth < 0 then mineWidth = 0 end + if otherWidth < 0 then otherWidth = 0 end + if mineWidth > availableWidth then mineWidth = availableWidth end + if otherWidth > (availableWidth - mineWidth) then + otherWidth = availableWidth - mineWidth + end + + if mineWidth > 0 then + predMine:ClearAllPoints() + predMine:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth, 0) + predMine:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth, 0) + predMine:SetWidth(mineWidth) + predMine:Show() + else + predMine:Hide() + end + + if otherWidth > 0 then + predOther:ClearAllPoints() + predOther:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth + mineWidth, 0) + predOther:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth + mineWidth, 0) + predOther:SetWidth(otherWidth) + predOther:Show() + else + predOther:Hide() + end +end + +function SFrames.Target:UpdatePowerType() + local powerType = UnitPowerType("target") + local color = SFrames.Config.colors.power[powerType] + if color then + self.frame.power:SetStatusBarColor(color.r, color.g, color.b) + else + self.frame.power:SetStatusBarColor(0, 0, 1) + end +end + +function SFrames.Target:UpdatePower() + local power = UnitMana("target") + local maxPower = UnitManaMax("target") + self.frame.power:SetMinMaxValues(0, maxPower) + self.frame.power:SetValue(power) + if maxPower > 0 then + self.frame.powerText:SetText(power .. " / " .. maxPower) + else + self.frame.powerText:SetText("") + end +end + +function SFrames.Target:UpdateComboPoints() + local points = GetComboPoints() + if points > 0 then + self.frame.comboText:SetText(points) + else + self.frame.comboText:SetText("") + end +end + +function SFrames.Target:UpdateRaidIcon() + if not (self.frame and self.frame.raidIcon) then return end + if not GetRaidTargetIndex then + self.frame.raidIcon:Hide() + return + end + if not UnitExists("target") then + self.frame.raidIcon:Hide() + return + end + local index = GetRaidTargetIndex("target") + if index and index > 0 and index <= 8 then + local col = math.mod(index - 1, 4) + local row = math.floor((index - 1) / 4) + self.frame.raidIcon:SetTexCoord(col * 0.25, (col + 1) * 0.25, row * 0.25, (row + 1) * 0.25) + self.frame.raidIcon:Show() + else + self.frame.raidIcon:Hide() + end +end + +-------------------------------------------------------------------------------- +-- Target Auras (Buffs / Debuffs) +-------------------------------------------------------------------------------- + +function SFrames.Target:CreateAuras() + self.frame.buffs = {} + self.frame.debuffs = {} + + -- Target Buffs (Top left to right) + for i = 1, 16 do + local b = CreateFrame("Button", "SFramesTargetBuff"..i, self.frame) + b:SetWidth(AURA_SIZE) + b:SetHeight(AURA_SIZE) + SFrames:CreateUnitBackdrop(b) + + b.icon = b:CreateTexture(nil, "ARTWORK") + b.icon:SetAllPoints() + b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + + b.cdText = SFrames:CreateFontString(b, 9, "CENTER") + b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1) + b.cdText:SetTextColor(1, 0.82, 0) + b.cdText:SetShadowColor(0, 0, 0, 1) + b.cdText:SetShadowOffset(1, -1) + + -- Tooltip support + b:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") + GameTooltip:SetUnitBuff("target", this:GetID()) + end) + b:SetScript("OnLeave", function() GameTooltip:Hide() end) + + -- Default row anchor (Starting bottom left for buffs) + if i == 1 then + b:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", 0, -1) + elseif math.mod(i - 1, 8) == 0 then + b:SetPoint("TOP", self.frame.buffs[i-8], "BOTTOM", 0, -AURA_ROW_SPACING) + else + b:SetPoint("LEFT", self.frame.buffs[i-1], "RIGHT", AURA_SPACING, 0) + end + b:Hide() + self.frame.buffs[i] = b + end + + -- Target Debuffs (Bottom left to right) + for i = 1, 16 do + local b = CreateFrame("Button", "SFramesTargetDebuff"..i, self.frame) + b:SetWidth(AURA_SIZE) + b:SetHeight(AURA_SIZE) + SFrames:CreateUnitBackdrop(b) + + b.icon = b:CreateTexture(nil, "ARTWORK") + b.icon:SetAllPoints() + b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + + b.cdText = SFrames:CreateFontString(b, 9, "CENTER") + b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1) + b.cdText:SetTextColor(1, 0.82, 0) + b.cdText:SetShadowColor(0, 0, 0, 1) + b.cdText:SetShadowOffset(1, -1) + + -- Tooltip support + b:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") + GameTooltip:SetUnitDebuff("target", this:GetID()) + end) + b:SetScript("OnLeave", function() GameTooltip:Hide() end) + b:SetScript("OnLeave", function() GameTooltip:Hide() end) + + -- Debuff anchors are recalulated dynamically in UpdateAuras + if i == 1 then + b:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", 0, -1) + elseif math.mod(i - 1, 8) == 0 then + b:SetPoint("TOP", self.frame.debuffs[i-8], "BOTTOM", 0, -AURA_ROW_SPACING) + else + b:SetPoint("LEFT", self.frame.debuffs[i-1], "RIGHT", AURA_SPACING, 0) + end + b:Hide() + self.frame.debuffs[i] = b + end + + SFrames:RegisterEvent("UNIT_AURA", function() if arg1 == "target" then self:UpdateAuras() end end) + + self.auraUpdater = CreateFrame("Frame", nil, self.frame) + self.auraUpdater.timer = 0 + self.auraUpdater:SetScript("OnUpdate", function() + this.timer = this.timer + arg1 + if this.timer >= 0.25 then + SFrames.Target:TickAuras() + SFrames.Target:UpdateHealPrediction() + this.timer = 0 + end + end) +end + +function SFrames.Target:TickAuras() + if not UnitExists("target") then return end + + local timeNow = GetTime() + + for i = 1, 16 do + local b = self.frame.buffs[i] + if b:IsShown() and b.expirationTime then + local timeLeft = b.expirationTime - timeNow + if timeLeft > 0 and timeLeft < 3600 then + b.cdText:SetText(SFrames:FormatTime(timeLeft)) + else + b.cdText:SetText("") + end + end + end + + for i = 1, 16 do + local b = self.frame.debuffs[i] + if b:IsShown() and b.expirationTime then + local timeLeft = b.expirationTime - timeNow + if timeLeft > 0 and timeLeft < 3600 then + b.cdText:SetText(SFrames:FormatTime(timeLeft)) + else + b.cdText:SetText("") + end + end + end +end + +function SFrames.Target:UpdateAuras() + if not UnitExists("target") then return end + + local numBuffs = 0 + -- Buffs + for i = 1, 16 do + local texture = UnitBuff("target", i) + local b = self.frame.buffs[i] + b:SetID(i) -- Ensure ID is set for tooltips + if texture then + b.icon:SetTexture(texture) + + -- Scrape tooltip for duration + SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") + SFrames.Tooltip:ClearLines() + SFrames.Tooltip:SetUnitBuff("target", i) + local timeLeft = SFrames:GetAuraTimeLeft("target", i, true) + if timeLeft and timeLeft > 0 then + b.expirationTime = GetTime() + timeLeft + b.cdText:SetText(SFrames:FormatTime(timeLeft)) + else + b.expirationTime = nil + b.cdText:SetText("") + end + + b:Show() + numBuffs = numBuffs + 1 + else + b.expirationTime = nil + b.cdText:SetText("") + b:Hide() + end + end + + -- Dynamically re-anchor the first Debuff based on visible Buffs + local firstDebuff = self.frame.debuffs[1] + if firstDebuff then + firstDebuff:ClearAllPoints() + if numBuffs > 0 then + -- Find the start of the LAST row of buffs + local lastRowStart = math.floor((numBuffs - 1) / 8) * 8 + 1 + firstDebuff:SetPoint("TOP", self.frame.buffs[lastRowStart], "BOTTOM", 0, -AURA_ROW_SPACING) + else + firstDebuff:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", 0, -1) + end + end + + -- Debuffs + for i = 1, 16 do + local texture = UnitDebuff("target", i) + local b = self.frame.debuffs[i] + b:SetID(i) -- Ensure ID is set for tooltips + if texture then + b.icon:SetTexture(texture) + + -- Scrape tooltip for duration + SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") + SFrames.Tooltip:ClearLines() + SFrames.Tooltip:SetUnitDebuff("target", i) + local timeLeft = SFrames:GetAuraTimeLeft("target", i, false) + if timeLeft and timeLeft > 0 then + b.expirationTime = GetTime() + timeLeft + b.cdText:SetText(SFrames:FormatTime(timeLeft)) + else + b.expirationTime = nil + b.cdText:SetText("") + end + + b:Show() + else + b.expirationTime = nil + b.cdText:SetText("") + b:Hide() + end + end +end + +-------------------------------------------------------------------------------- +-- Target Castbar +-------------------------------------------------------------------------------- + +function SFrames.Target:CreateCastbar() + local cb = SFrames:CreateStatusBar(self.frame, "SFramesTargetCastbar") + cb:SetHeight(SFrames.Config.castbarHeight) + cb:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", 0, 6) + cb:SetPoint("BOTTOMRIGHT", self.frame.portrait, "TOPRIGHT", -(SFrames.Config.castbarHeight + 6), 6) + + local cbbg = CreateFrame("Frame", nil, self.frame) + cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1) + cbbg:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 1, -1) + cbbg:SetFrameLevel(cb:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(cbbg) + + cb.bg = cb:CreateTexture(nil, "BACKGROUND") + cb.bg:SetAllPoints() + cb.bg:SetTexture(SFrames:GetTexture()) + cb.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + cb:SetStatusBarColor(1, 0.7, 0) + + cb.text = SFrames:CreateFontString(cb, 10, "LEFT") + cb.text:SetPoint("LEFT", cb, "LEFT", 4, 0) + + cb.icon = cb:CreateTexture(nil, "ARTWORK") + cb.icon:SetWidth(SFrames.Config.castbarHeight + 2) + cb.icon:SetHeight(SFrames.Config.castbarHeight + 2) + cb.icon:SetPoint("LEFT", cb, "RIGHT", 4, 0) + cb.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + + local ibg = CreateFrame("Frame", nil, self.frame) + ibg:SetPoint("TOPLEFT", cb.icon, "TOPLEFT", -1, 1) + ibg:SetPoint("BOTTOMRIGHT", cb.icon, "BOTTOMRIGHT", 1, -1) + ibg:SetFrameLevel(cb:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(ibg) + + cb:Hide() + cbbg:Hide() + cb.icon:Hide() + ibg:Hide() + + self.frame.castbar = cb + self.frame.castbar.cbbg = cbbg + self.frame.castbar.ibg = ibg + + self.frame.castbarUpdater = CreateFrame("Frame", nil, self.frame) + self.frame.castbarUpdater:SetScript("OnUpdate", function() SFrames.Target:CastbarOnUpdate() end) +end + +function SFrames.Target:CastbarOnUpdate() + local cb = self.frame.castbar + if not UnitExists("target") then + cb:Hide() + cb.cbbg:Hide() + cb.icon:Hide() + cb.ibg:Hide() + return + end + + -- Try to read cast from Vanilla extensions (SuperWoW or TurtleWoW modern API, or ShaguTweaks) + local cast, nameSubtext, text, texture, startTime, endTime + local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo) + if _UnitCastingInfo then + cast, nameSubtext, text, texture, startTime, endTime = _UnitCastingInfo("target") + end + + local channel + local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo) + if not cast and _UnitChannelInfo then + channel, nameSubtext, text, texture, startTime, endTime = _UnitChannelInfo("target") + cast = channel + end + + if cast and startTime and endTime then + local duration = (endTime - startTime) / 1000 + local cur = GetTime() - (startTime / 1000) + + if channel then + cur = duration + (startTime / 1000) - GetTime() + end + + if cur > duration then cur = duration end + if cur < 0 then cur = 0 end + + cb:SetMinMaxValues(0, duration) + cb:SetValue(cur) + cb.text:SetText(cast) + + if texture then + cb.icon:SetTexture(texture) + end + + cb:SetAlpha(1) + cb.cbbg:SetAlpha(1) + cb.icon:SetAlpha(1) + cb.ibg:SetAlpha(1) + + cb:Show() + cb.cbbg:Show() + cb.icon:Show() + cb.ibg:Show() + else + cb:Hide() + cb.cbbg:Hide() + cb.icon:Hide() + cb.ibg:Hide() + end +end + +-- Diagnostic Slash Command (Position Recovery) +SLASH_NANAMIDIST1 = "/nanamidist" +SlashCmdList["NANAMIDIST"] = function() + if SFrames.Target.distanceFrame then + SFrames.Target.distanceFrame:ClearAllPoints() + SFrames.Target.distanceFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 100) + SFrames.Target.distanceFrame:Show() + DEFAULT_CHAT_FRAME:AddMessage("|cffffd100Nanami-UI:|r 距离显示已重置到屏幕中央。") + end +end diff --git a/Units/ToT.lua b/Units/ToT.lua new file mode 100644 index 0000000..0519d0d --- /dev/null +++ b/Units/ToT.lua @@ -0,0 +1,85 @@ +SFrames.ToT = {} +local _A = SFrames.ActiveTheme + +function SFrames.ToT:Initialize() + local f = CreateFrame("Button", "SFramesToTFrame", UIParent) + f:SetWidth(120) + f:SetHeight(25) + f:SetPoint("BOTTOMLEFT", SFramesTargetFrame, "BOTTOMRIGHT", 5, 0) + + f:RegisterForClicks("LeftButtonUp", "RightButtonUp") + f:SetScript("OnClick", function() + if arg1 == "LeftButton" then + TargetUnit("targettarget") + end + end) + + -- Health Bar + f.health = SFrames:CreateStatusBar(f, "SFramesToTHealth") + f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) + f.health:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1) + + local hbg = CreateFrame("Frame", nil, f) + hbg:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1) + hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1) + hbg:SetFrameLevel(f:GetFrameLevel() - 1) + SFrames:CreateUnitBackdrop(hbg) + + f.health.bg = f.health:CreateTexture(nil, "BACKGROUND") + f.health.bg:SetAllPoints() + f.health.bg:SetTexture(SFrames:GetTexture()) + f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) + + f.nameText = SFrames:CreateFontString(f.health, 10, "CENTER") + f.nameText:SetPoint("CENTER", f.health, "CENTER", 0, 0) + + self.frame = f + f:Hide() + + -- Update loop since targettarget changes don't fire precise events in Vanilla + self.updater = CreateFrame("Frame") + self.updater.timer = 0 + self.updater:SetScript("OnUpdate", function() + this.timer = this.timer + arg1 + if this.timer >= 0.2 then + SFrames.ToT:Update() + this.timer = 0 + end + end) +end + +function SFrames.ToT:Update() + if UnitExists("targettarget") then + self.frame:Show() + + local hp = UnitHealth("targettarget") + local maxHp = UnitHealthMax("targettarget") + self.frame.health:SetMinMaxValues(0, maxHp) + self.frame.health:SetValue(hp) + + self.frame.nameText:SetText(UnitName("targettarget")) + + if UnitIsPlayer("targettarget") then + local _, class = UnitClass("targettarget") + local color = SFrames.Config.colors.class[class] + if color then + self.frame.health:SetStatusBarColor(color.r, color.g, color.b) + self.frame.nameText:SetTextColor(color.r, color.g, color.b) + else + self.frame.health:SetStatusBarColor(0, 1, 0) + self.frame.nameText:SetTextColor(1, 1, 1) + end + else + local r, g, b = 0.85, 0.77, 0.36 -- Neutral + if UnitIsEnemy("player", "targettarget") then + r, g, b = 0.78, 0.25, 0.25 -- Enemy + elseif UnitIsFriend("player", "targettarget") then + r, g, b = 0.33, 0.59, 0.33 -- Friend + end + self.frame.health:SetStatusBarColor(r, g, b) + self.frame.nameText:SetTextColor(r, g, b) + end + else + self.frame:Hide() + end +end diff --git a/Whisper.lua b/Whisper.lua new file mode 100644 index 0000000..556bc0d --- /dev/null +++ b/Whisper.lua @@ -0,0 +1,882 @@ +local CFG_THEME = SFrames.ActiveTheme + +SFrames.Whisper = SFrames.Whisper or {} +SFrames.Whisper.history = SFrames.Whisper.history or {} +SFrames.Whisper.contacts = SFrames.Whisper.contacts or {} +SFrames.Whisper.unreadCount = SFrames.Whisper.unreadCount or {} +SFrames.Whisper.activeContact = nil + +local MAX_WHISPER_CONTACTS = 200 +local MAX_MESSAGES_PER_CONTACT = 100 + +function SFrames.Whisper:SaveCache() + if not SFramesDB then SFramesDB = {} end + SFramesDB.whisperContacts = {} + SFramesDB.whisperHistory = {} + for _, contact in ipairs(self.contacts) do + table.insert(SFramesDB.whisperContacts, contact) + if self.history[contact] then + local msgs = self.history[contact] + -- Only persist the last MAX_MESSAGES_PER_CONTACT messages per contact + local start = math.max(1, table.getn(msgs) - MAX_MESSAGES_PER_CONTACT + 1) + local trimmed = {} + for i = start, table.getn(msgs) do + table.insert(trimmed, { time = msgs[i].time, text = msgs[i].text, isMe = msgs[i].isMe, translated = msgs[i].translated }) + end + SFramesDB.whisperHistory[contact] = trimmed + end + end +end + +function SFrames.Whisper:LoadCache() + if not SFramesDB then return end + if type(SFramesDB.whisperContacts) ~= "table" or type(SFramesDB.whisperHistory) ~= "table" then return end + + self.contacts = {} + self.history = {} + for _, contact in ipairs(SFramesDB.whisperContacts) do + if type(contact) == "string" and SFramesDB.whisperHistory[contact] and table.getn(SFramesDB.whisperHistory[contact]) > 0 then + table.insert(self.contacts, contact) + self.history[contact] = SFramesDB.whisperHistory[contact] + end + end + -- Trim to max contacts (remove oldest = last in list) + while table.getn(self.contacts) > MAX_WHISPER_CONTACTS do + local oldest = table.remove(self.contacts) + if oldest then + self.history[oldest] = nil + end + end +end + +function SFrames.Whisper:RemoveContact(contact) + for i, v in ipairs(self.contacts) do + if v == contact then + table.remove(self.contacts, i) + break + end + end + self.history[contact] = nil + self.unreadCount[contact] = nil + if self.activeContact == contact then + self.activeContact = self.contacts[1] + end + self:SaveCache() + if self.frame and self.frame:IsShown() then + self:UpdateContacts() + self:UpdateMessages() + end +end + +function SFrames.Whisper:ClearAllHistory() + self.history = {} + self.contacts = {} + self.unreadCount = {} + self.activeContact = nil + self:SaveCache() + if self.frame and self.frame:IsShown() then + self:UpdateContacts() + self:UpdateMessages() + end +end + +local function FormatTime() + local h, m = GetGameTime() + return string.format("%02d:%02d", h, m) +end + +local function GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +local function SetRoundBackdrop(frame, bgColor, borderColor) + frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + local bg = bgColor or CFG_THEME.panelBg + local bd = borderColor or CFG_THEME.panelBorder + frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) + frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) +end + +local function CreateShadow(parent, size) + local s = CreateFrame("Frame", nil, parent) + local sz = size or 4 + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz) + s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0)) + s:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + s:SetBackdropColor(0, 0, 0, 0.55) + s:SetBackdropBorderColor(0, 0, 0, 0.4) + return s +end + +local function EnsureBackdrop(frame) + if not frame then return end + if frame.sfCfgBackdrop then return end + SetRoundBackdrop(frame) + frame.sfCfgBackdrop = true +end + +local function StyleActionButton(btn, label) + SetRoundBackdrop(btn, CFG_THEME.buttonBg, CFG_THEME.buttonBorder) + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(GetFont(), 12, "OUTLINE") + fs:SetTextColor(CFG_THEME.btnText[1], CFG_THEME.btnText[2], CFG_THEME.btnText[3]) + fs:SetPoint("CENTER", btn, "CENTER", 0, 0) + if label then fs:SetText(label) end + btn.nLabel = fs + + btn:SetScript("OnEnter", function() + this:SetBackdropColor(CFG_THEME.buttonHoverBg[1], CFG_THEME.buttonHoverBg[2], CFG_THEME.buttonHoverBg[3], CFG_THEME.buttonHoverBg[4]) + this:SetBackdropBorderColor(CFG_THEME.btnHoverBorder[1], CFG_THEME.btnHoverBorder[2], CFG_THEME.btnHoverBorder[3], CFG_THEME.btnHoverBorder[4]) + if this.nLabel then this.nLabel:SetTextColor(CFG_THEME.btnActiveText[1], CFG_THEME.btnActiveText[2], CFG_THEME.btnActiveText[3]) end + end) + btn:SetScript("OnLeave", function() + this:SetBackdropColor(CFG_THEME.buttonBg[1], CFG_THEME.buttonBg[2], CFG_THEME.buttonBg[3], CFG_THEME.buttonBg[4]) + this:SetBackdropBorderColor(CFG_THEME.buttonBorder[1], CFG_THEME.buttonBorder[2], CFG_THEME.buttonBorder[3], CFG_THEME.buttonBorder[4]) + if this.nLabel then this.nLabel:SetTextColor(CFG_THEME.btnText[1], CFG_THEME.btnText[2], CFG_THEME.btnText[3]) end + end) + btn:SetScript("OnMouseDown", function() + this:SetBackdropColor(CFG_THEME.buttonDownBg[1], CFG_THEME.buttonDownBg[2], CFG_THEME.buttonDownBg[3], CFG_THEME.buttonDownBg[4]) + end) + btn:SetScript("OnMouseUp", function() + this:SetBackdropColor(CFG_THEME.buttonHoverBg[1], CFG_THEME.buttonHoverBg[2], CFG_THEME.buttonHoverBg[3], CFG_THEME.buttonHoverBg[4]) + end) + return fs +end + +local function IsNotFullyChinese(text) + -- If there's any english letter, we consider it requires translation + return string.find(text, "[a-zA-Z]") ~= nil +end + +local function TranslateMessage(text, callback) + if SFramesDB and SFramesDB.Chat and SFramesDB.Chat.translateEnabled == false then + callback(nil) + return + end + if not IsNotFullyChinese(text) then + callback(nil) + return + end + + local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") + cleanText = string.gsub(cleanText, "|r", "") + cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1") + + if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then + _G.STranslateAPI.Translate(cleanText, "auto", "zh", function(result, err, meta) + if result then + callback(result) + end + end, "Nanami-UI") + else + callback(nil) + end +end + +function SFrames.Whisper:AddMessage(sender, text, isMe) + if not self.history[sender] then + self.history[sender] = {} + table.insert(self.contacts, 1, sender) + else + -- Move to front + for i, v in ipairs(self.contacts) do + if v == sender then + table.remove(self.contacts, i) + break + end + end + table.insert(self.contacts, 1, sender) + end + + local msgData = { + time = FormatTime(), + text = text, + isMe = isMe + } + table.insert(self.history[sender], msgData) + + if not isMe then + PlaySound("TellIncoming") + if self.activeContact ~= sender or not (self.frame and self.frame:IsShown()) then + self.unreadCount[sender] = (self.unreadCount[sender] or 0) + 1 + if SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame.whisperButton then + SFrames.Chat.frame.whisperButton.hasUnread = true + if SFrames.Chat.frame.whisperButton.flashFrame then + SFrames.Chat.frame.whisperButton.flashFrame:Show() + end + end + end + + TranslateMessage(text, function(translated) + if translated and translated ~= "" then + msgData.translated = "(翻译) " .. translated + if self.frame and self.frame:IsShown() and self.activeContact == sender then + self:UpdateMessages() + end + end + end) + end + + -- Trim contacts if exceeding max + while table.getn(self.contacts) > MAX_WHISPER_CONTACTS do + local oldest = table.remove(self.contacts) + if oldest then + self.history[oldest] = nil + self.unreadCount[oldest] = nil + end + end + + if self.frame and self.frame:IsShown() then + self:UpdateContacts() + if self.activeContact == sender then + self:UpdateMessages() + end + end + + self:SaveCache() +end + +function SFrames.Whisper:SelectContact(contact) + self.activeContact = contact + self.unreadCount[contact] = 0 + self:UpdateContacts() + self:UpdateMessages() + + -- Clear global unread state if no more unread + local totalUnread = 0 + for k, v in pairs(self.unreadCount) do + totalUnread = totalUnread + v + end + if totalUnread == 0 and SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame.whisperButton then + SFrames.Chat.frame.whisperButton.hasUnread = false + if SFrames.Chat.frame.whisperButton.flashFrame then + SFrames.Chat.frame.whisperButton.flashFrame:Hide() + end + end + + if self.frame and self.frame.editBox then + self.frame.editBox:SetText("") + self.frame.editBox:SetFocus() + end +end + +function SFrames.Whisper:UpdateContacts() + if not self.frame or not self.frame.contactList then return end + + local scrollChild = self.frame.contactList.scrollChild + for _, child in ipairs({scrollChild:GetChildren()}) do + child:Hide() + end + + local yOffset = -5 + local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + + for i, contact in ipairs(self.contacts) do + local btn = self.contactButtons and self.contactButtons[i] + if not btn then + btn = CreateFrame("Button", nil, scrollChild) + btn:SetWidth(120) + btn:SetHeight(24) + + EnsureBackdrop(btn) + + local txt = btn:CreateFontString(nil, "OVERLAY") + txt:SetFont(fontPath, 12, "OUTLINE") + txt:SetPoint("LEFT", btn, "LEFT", 8, 0) + btn.txt = txt + + local closeBtn = CreateFrame("Button", nil, btn) + closeBtn:SetWidth(16) + closeBtn:SetHeight(16) + closeBtn:SetPoint("RIGHT", btn, "RIGHT", -2, 0) + + local closeTxt = closeBtn:CreateFontString(nil, "OVERLAY") + closeTxt:SetFont(fontPath, 10, "OUTLINE") + closeTxt:SetPoint("CENTER", closeBtn, "CENTER", 0, 1) + closeTxt:SetText("x") + closeTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) + closeBtn:SetFontString(closeTxt) + closeBtn:SetScript("OnEnter", function() closeTxt:SetTextColor(1, 0.4, 0.5) end) + closeBtn:SetScript("OnLeave", function() closeTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end) + closeBtn:SetScript("OnClick", function() + SFrames.Whisper:RemoveContact(this:GetParent().contact) + end) + btn.closeBtn = closeBtn + + local unreadTxt = btn:CreateFontString(nil, "OVERLAY") + unreadTxt:SetFont(fontPath, 10, "OUTLINE") + unreadTxt:SetPoint("RIGHT", closeBtn, "LEFT", -2, 0) + unreadTxt:SetTextColor(1, 0.4, 0.4) + btn.unreadTxt = unreadTxt + + btn:SetScript("OnClick", function() + SFrames.Whisper:SelectContact(this.contact) + end) + + btn:SetScript("OnEnter", function() + if this.contact ~= SFrames.Whisper.activeContact then + this:SetBackdropColor(CFG_THEME.buttonHoverBg[1], CFG_THEME.buttonHoverBg[2], CFG_THEME.buttonHoverBg[3], CFG_THEME.buttonHoverBg[4]) + end + end) + btn:SetScript("OnLeave", function() + if this.contact ~= SFrames.Whisper.activeContact then + this:SetBackdropColor(0,0,0,0) + end + end) + + if not self.contactButtons then self.contactButtons = {} end + self.contactButtons[i] = btn + end + + btn.contact = contact + btn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 5, yOffset) + btn.txt:SetText(contact) + + local unreads = self.unreadCount[contact] or 0 + if unreads > 0 then + btn.unreadTxt:SetText("("..unreads..")") + btn.txt:SetTextColor(1, 0.8, 0.2) + else + btn.unreadTxt:SetText("") + btn.txt:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + end + + if contact == self.activeContact then + btn:SetBackdropColor(CFG_THEME.buttonBg[1], CFG_THEME.buttonBg[2], CFG_THEME.buttonBg[3], CFG_THEME.buttonBg[4]) + btn:SetBackdropBorderColor(CFG_THEME.buttonBorder[1], CFG_THEME.buttonBorder[2], CFG_THEME.buttonBorder[3], CFG_THEME.buttonBorder[4]) + else + btn:SetBackdropColor(0,0,0,0) + btn:SetBackdropBorderColor(0,0,0,0) + end + + btn:Show() + yOffset = yOffset - 26 + end + + scrollChild:SetHeight(math.abs(yOffset)) + self.frame.contactList:UpdateScrollChildRect() + local maxScroll = math.max(0, math.abs(yOffset) - 330) + local slider = _G["SFramesWhisperContactScrollScrollBar"] + if slider then + slider:SetMinMaxValues(0, maxScroll) + end +end + +function SFrames.Whisper:UpdateMessages() + if not self.frame or not self.frame.messageScroll then return end + + local scrollChild = self.frame.messageScroll.scrollChild + if self.msgLabels then + for i, fs in ipairs(self.msgLabels) do + fs:Hide() + end + end + if self.msgCopyBtns then + for i, btn in ipairs(self.msgCopyBtns) do + btn:Hide() + end + end + + if not self.activeContact then return end + local messages = self.history[self.activeContact] or {} + + local yOffset = -5 + local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + + if not self.msgLabels then self.msgLabels = {} end + if not self.msgCopyBtns then self.msgCopyBtns = {} end + + for i, msg in ipairs(messages) do + local fs = self.msgLabels[i] + if not fs then + fs = scrollChild:CreateFontString(nil, "OVERLAY") + fs:SetFont(fontPath, 13, "OUTLINE") + fs:SetJustifyH("LEFT") + fs:SetWidth(380) + if fs.EnableMouse then fs:EnableMouse(true) end + self.msgLabels[i] = fs + end + + local btn = self.msgCopyBtns[i] + if not btn then + btn = CreateFrame("Button", nil, scrollChild) + btn:SetWidth(20) + btn:SetHeight(16) + local txt = btn:CreateFontString(nil, "OVERLAY") + txt:SetFont(fontPath, 13, "OUTLINE") + txt:SetPoint("CENTER", btn, "CENTER", 0, 0) + txt:SetText("[+]") + txt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) + btn:SetFontString(txt) + + btn:SetScript("OnEnter", function() txt:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) end) + btn:SetScript("OnLeave", function() txt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end) + btn:SetScript("OnClick", function() + if SFrames and SFrames.Chat and SFrames.Chat.OpenMessageContextMenu then + SFrames.Chat:OpenMessageContextMenu("whisper", this.rawText, this.senderName) + end + end) + self.msgCopyBtns[i] = btn + end + + local color = msg.isMe and "|cff66ccff" or "|cffffbbee" + local nameStr = msg.isMe and "我" or self.activeContact + local textStr = string.format("%s[%s] %s:|r %s", color, msg.time, nameStr, msg.text) + + if msg.translated then + textStr = textStr .. "\n|cffaaaaaa" .. msg.translated .. "|r" + end + + btn.rawText = msg.text + btn.senderName = nameStr + btn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 5, yOffset) + btn:Show() + + fs:SetText(textStr) + fs:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 28, yOffset) + fs:Show() + + yOffset = yOffset - fs:GetHeight() - 8 + end + + scrollChild:SetHeight(math.abs(yOffset)) + self.frame.messageScroll:UpdateScrollChildRect() + local maxScroll = math.max(0, math.abs(yOffset) - 270) + local slider = _G["SFramesWhisperMessageScrollScrollBar"] + if slider then + slider:SetMinMaxValues(0, maxScroll) + slider:SetValue(maxScroll) + end + self.frame.messageScroll:SetVerticalScroll(maxScroll) +end + +function SFrames.Whisper:EnsureFrame() + if self.frame then return end + + local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + + local f = CreateFrame("Frame", "SFramesWhisperContainer", UIParent) + f:SetWidth(580) + f:SetHeight(380) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 100) + f:SetFrameStrata("HIGH") + f:SetMovable(true) + f:EnableMouse(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + if not self.enterHooked then + self.enterHooked = true + local orig_ChatFrame_OpenChat = ChatFrame_OpenChat + if orig_ChatFrame_OpenChat then + ChatFrame_OpenChat = function(text, chatFrame) + if SFrames and SFrames.Whisper and SFrames.Whisper.frame and SFrames.Whisper.frame:IsShown() and (not text or text == "") then + SFrames.Whisper.frame.editBox:SetFocus() + return + end + orig_ChatFrame_OpenChat(text, chatFrame) + end + end + end + + table.insert(UISpecialFrames, "SFramesWhisperContainer") + + SetRoundBackdrop(f, CFG_THEME.panelBg, CFG_THEME.panelBorder) + CreateShadow(f, 5) + + local titleIcon = SFrames:CreateIcon(f, "chat", 14) + titleIcon:SetDrawLayer("OVERLAY") + titleIcon:SetPoint("TOPLEFT", f, "TOPLEFT", 15, -12) + titleIcon:SetVertexColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) + + local title = f:CreateFontString(nil, "OVERLAY") + title:SetFont(fontPath, 14, "OUTLINE") + title:SetPoint("LEFT", titleIcon, "RIGHT", 4, 0) + title:SetText("私聊对话管理") + title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) + + local close = CreateFrame("Button", nil, f) + close:SetWidth(18) + close:SetHeight(18) + close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8) + close:SetFrameLevel(f:GetFrameLevel() + 3) + SetRoundBackdrop(close, CFG_THEME.buttonDownBg or { 0.35, 0.06, 0.06, 0.85 }, CFG_THEME.buttonBorder or { 0.45, 0.1, 0.1, 0.6 }) + local closeIco = SFrames:CreateIcon(close, "close", 12) + closeIco:SetDrawLayer("OVERLAY") + closeIco:SetPoint("CENTER", close, "CENTER", 0, 0) + closeIco:SetVertexColor(1, 0.7, 0.7) + close:SetScript("OnClick", function() f:Hide() end) + close:SetScript("OnEnter", function() + local h = CFG_THEME.buttonHoverBg or { 0.55, 0.1, 0.1, 0.95 } + local hb = CFG_THEME.btnHoverBd or { 0.65, 0.15, 0.15, 0.9 } + this:SetBackdropColor(h[1], h[2], h[3], h[4] or 0.95) + this:SetBackdropBorderColor(hb[1], hb[2], hb[3], hb[4] or 0.9) + end) + close:SetScript("OnLeave", function() + local d = CFG_THEME.buttonDownBg or { 0.35, 0.06, 0.06, 0.85 } + local db = CFG_THEME.buttonBorder or { 0.45, 0.1, 0.1, 0.6 } + this:SetBackdropColor(d[1], d[2], d[3], d[4] or 0.85) + this:SetBackdropBorderColor(db[1], db[2], db[3], db[4] or 0.6) + end) + + -- Contact List + local contactList = CreateFrame("ScrollFrame", "SFramesWhisperContactScroll", f, "UIPanelScrollFrameTemplate") + contactList:SetWidth(130) + contactList:SetHeight(330) + contactList:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -40) + SetRoundBackdrop(contactList, CFG_THEME.listBg, CFG_THEME.listBorder) + + local contactChild = CreateFrame("Frame", nil, contactList) + contactChild:SetWidth(130) + contactChild:SetHeight(10) + contactList:SetScrollChild(contactChild) + contactList.scrollChild = contactChild + f.contactList = contactList + + -- Apply Nanami-UI scrollbar styling + local contactSlider = _G["SFramesWhisperContactScrollScrollBar"] + if contactSlider then + contactSlider:SetWidth(12) + local regions = { contactSlider:GetRegions() } + for i = 1, table.getn(regions) do + local region = regions[i] + if region and region.GetObjectType and region:GetObjectType() == "Texture" then + region:SetTexture(nil) + end + end + local track = contactSlider:CreateTexture(nil, "BACKGROUND") + track:SetTexture("Interface\\Buttons\\WHITE8X8") + track:SetPoint("TOPLEFT", contactSlider, "TOPLEFT", 3, 0) + track:SetPoint("BOTTOMRIGHT", contactSlider, "BOTTOMRIGHT", -3, 0) + track:SetVertexColor(0.22, 0.12, 0.18, 0.9) + + if contactSlider.SetThumbTexture then + contactSlider:SetThumbTexture("Interface\\Buttons\\WHITE8X8") + end + local thumb = contactSlider.GetThumbTexture and contactSlider:GetThumbTexture() + if thumb then + thumb:SetWidth(8) + thumb:SetHeight(20) + thumb:SetVertexColor(0.85, 0.55, 0.70, 0.95) + end + + local upBtn = _G["SFramesWhisperContactScrollScrollBarScrollUpButton"] + local downBtn = _G["SFramesWhisperContactScrollScrollBarScrollDownButton"] + if upBtn then upBtn:Hide() end + if downBtn then downBtn:Hide() end + + contactSlider:ClearAllPoints() + contactSlider:SetPoint("TOPRIGHT", contactList, "TOPRIGHT", -2, -6) + contactSlider:SetPoint("BOTTOMRIGHT", contactList, "BOTTOMRIGHT", -2, 6) + end + + -- Message List + local messageScroll = CreateFrame("ScrollFrame", "SFramesWhisperMessageScroll", f, "UIPanelScrollFrameTemplate") + messageScroll:SetWidth(405) + messageScroll:SetHeight(270) + messageScroll:SetPoint("TOPLEFT", contactList, "TOPRIGHT", 5, 0) + SetRoundBackdrop(messageScroll, CFG_THEME.listBg, CFG_THEME.listBorder) + + local messageChild = CreateFrame("Frame", nil, messageScroll) + messageChild:SetWidth(405) + messageChild:SetHeight(10) + messageChild:EnableMouse(true) + -- Hyperlink 脚本仅部分帧类型支持,pcall 防止不支持的客户端报错 + pcall(function() + messageChild:SetScript("OnHyperlinkClick", function() + local link = arg1 + if not link then return end + if IsShiftKeyDown() then + if ChatFrameEditBox and ChatFrameEditBox:IsShown() then + ChatFrameEditBox:Insert(link) + elseif SFrames.Whisper.frame and SFrames.Whisper.frame.editBox then + SFrames.Whisper.frame.editBox:Insert(link) + end + else + pcall(function() SetItemRef(link, arg2, arg3) end) + end + end) + end) + pcall(function() + messageChild:SetScript("OnHyperlinkEnter", function() + local link = arg1 + if not link then return end + GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR") + local ok = pcall(function() GameTooltip:SetHyperlink(link) end) + if ok then + GameTooltip:Show() + else + GameTooltip:Hide() + end + end) + end) + pcall(function() + messageChild:SetScript("OnHyperlinkLeave", function() + GameTooltip:Hide() + end) + end) + messageScroll:SetScrollChild(messageChild) + messageScroll.scrollChild = messageChild + f.messageScroll = messageScroll + + local messageSlider = _G["SFramesWhisperMessageScrollScrollBar"] + if messageSlider then + messageSlider:SetWidth(12) + local regions = { messageSlider:GetRegions() } + for i = 1, table.getn(regions) do + local region = regions[i] + if region and region.GetObjectType and region:GetObjectType() == "Texture" then + region:SetTexture(nil) + end + end + local track = messageSlider:CreateTexture(nil, "BACKGROUND") + track:SetTexture("Interface\\Buttons\\WHITE8X8") + track:SetPoint("TOPLEFT", messageSlider, "TOPLEFT", 3, 0) + track:SetPoint("BOTTOMRIGHT", messageSlider, "BOTTOMRIGHT", -3, 0) + track:SetVertexColor(0.22, 0.12, 0.18, 0.9) + + if messageSlider.SetThumbTexture then + messageSlider:SetThumbTexture("Interface\\Buttons\\WHITE8X8") + end + local thumb = messageSlider.GetThumbTexture and messageSlider:GetThumbTexture() + if thumb then + thumb:SetWidth(8) + thumb:SetHeight(20) + thumb:SetVertexColor(0.85, 0.55, 0.70, 0.95) + end + + local upBtn = _G["SFramesWhisperMessageScrollScrollBarScrollUpButton"] + local downBtn = _G["SFramesWhisperMessageScrollScrollBarScrollDownButton"] + if upBtn then upBtn:Hide() end + if downBtn then downBtn:Hide() end + + messageSlider:ClearAllPoints() + messageSlider:SetPoint("TOPRIGHT", messageScroll, "TOPRIGHT", -2, -22) + messageSlider:SetPoint("BOTTOMRIGHT", messageScroll, "BOTTOMRIGHT", -2, 22) + + local topBtn = CreateFrame("Button", nil, messageScroll) + topBtn:SetWidth(12) + topBtn:SetHeight(12) + topBtn:SetPoint("BOTTOM", messageSlider, "TOP", 0, 4) + local topTxt = topBtn:CreateFontString(nil, "OVERLAY") + topTxt:SetFont(fontPath, 11, "OUTLINE") + topTxt:SetPoint("CENTER", topBtn, "CENTER", 0, 1) + topTxt:SetText("▲") + topTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) + topBtn:SetFontString(topTxt) + topBtn:SetScript("OnEnter", function() topTxt:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) end) + topBtn:SetScript("OnLeave", function() topTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end) + topBtn:SetScript("OnClick", function() + messageSlider:SetValue(0) + end) + + local bottomBtn = CreateFrame("Button", nil, messageScroll) + bottomBtn:SetWidth(12) + bottomBtn:SetHeight(12) + bottomBtn:SetPoint("TOP", messageSlider, "BOTTOM", 0, -4) + local botTxt = bottomBtn:CreateFontString(nil, "OVERLAY") + botTxt:SetFont(fontPath, 11, "OUTLINE") + botTxt:SetPoint("CENTER", bottomBtn, "CENTER", 0, -1) + botTxt:SetText("▼") + botTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) + bottomBtn:SetFontString(botTxt) + bottomBtn:SetScript("OnEnter", function() botTxt:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) end) + bottomBtn:SetScript("OnLeave", function() botTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end) + bottomBtn:SetScript("OnClick", function() + local _, maxVal = messageSlider:GetMinMaxValues() + messageSlider:SetValue(maxVal) + end) + + if messageScroll.EnableMouseWheel then + messageScroll:EnableMouseWheel(true) + messageScroll:SetScript("OnMouseWheel", function() + local delta = arg1 + local _, maxVal = messageSlider:GetMinMaxValues() + local val = messageSlider:GetValue() - delta * 40 + if val < 0 then val = 0 end + if val > maxVal then val = maxVal end + messageSlider:SetValue(val) + end) + end + end + + local contactSlider = _G["SFramesWhisperContactScrollScrollBar"] + if contactList and contactSlider and contactList.EnableMouseWheel then + contactList:EnableMouseWheel(true) + contactList:SetScript("OnMouseWheel", function() + local delta = arg1 + local _, maxVal = contactSlider:GetMinMaxValues() + local val = contactSlider:GetValue() - delta * 30 + if val < 0 then val = 0 end + if val > maxVal then val = maxVal end + contactSlider:SetValue(val) + end) + end + + -- EditBox for replying + local editBox = CreateFrame("EditBox", "SFramesWhisperEditBox", f, "InputBoxTemplate") + editBox:SetWidth(405) + editBox:SetHeight(20) + editBox:SetPoint("TOPLEFT", messageScroll, "BOTTOMLEFT", 0, -10) + editBox:SetAutoFocus(false) + editBox:SetFont(fontPath, 13, "OUTLINE") + + local translateCheck = CreateFrame("CheckButton", "SFramesWhisperTranslateCheck", f, "UICheckButtonTemplate") + translateCheck:SetWidth(24) + translateCheck:SetHeight(24) + translateCheck:SetPoint("TOPLEFT", editBox, "BOTTOMLEFT", -5, -6) + + local txt = translateCheck:CreateFontString(nil, "OVERLAY") + txt:SetFont(fontPath, 11, "OUTLINE") + txt:SetPoint("LEFT", translateCheck, "RIGHT", 2, 0) + txt:SetText("发前自动翻译 (译后可修改再回车)") + txt:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3]) + + -- Hide the default text and reskin the button if you have defined StyleCfgCheck inside your environment + if _G["SFramesWhisperTranslateCheckText"] then + _G["SFramesWhisperTranslateCheckText"]:Hide() + end + -- Reskin checkbox + local function StyleCheck(cb) + local box = CreateFrame("Frame", nil, cb) + box:SetPoint("TOPLEFT", cb, "TOPLEFT", 2, -2) + box:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -2, 2) + local boxLevel = (cb:GetFrameLevel() or 1) - 1 + if boxLevel < 0 then boxLevel = 0 end + box:SetFrameLevel(boxLevel) + EnsureBackdrop(box) + if box.SetBackdropColor then + box:SetBackdropColor(CFG_THEME.buttonBg[1], CFG_THEME.buttonBg[2], CFG_THEME.buttonBg[3], CFG_THEME.buttonBg[4]) + end + if box.SetBackdropBorderColor then + box:SetBackdropBorderColor(CFG_THEME.buttonBorder[1], CFG_THEME.buttonBorder[2], CFG_THEME.buttonBorder[3], CFG_THEME.buttonBorder[4]) + end + cb.sfBox = box + + if cb.GetNormalTexture and cb:GetNormalTexture() then cb:GetNormalTexture():Hide() end + if cb.GetPushedTexture and cb:GetPushedTexture() then cb:GetPushedTexture():Hide() end + if cb.GetHighlightTexture and cb:GetHighlightTexture() then cb:GetHighlightTexture():Hide() end + + if cb.SetCheckedTexture then cb:SetCheckedTexture("Interface\\Buttons\\WHITE8X8") end + local checked = cb.GetCheckedTexture and cb:GetCheckedTexture() + if checked then + checked:ClearAllPoints() + checked:SetPoint("TOPLEFT", cb, "TOPLEFT", 5, -5) + checked:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -5, 5) + checked:SetVertexColor(CFG_THEME.checkFill[1], CFG_THEME.checkFill[2], CFG_THEME.checkFill[3], CFG_THEME.checkFill[4]) + end + end + StyleCheck(translateCheck) + + f.translateCheck = translateCheck + + local function SendReply() + if SFrames.Whisper.isTranslating then return end + + local text = editBox:GetText() + if not text or text == "" or not SFrames.Whisper.activeContact then return end + + if f.translateCheck and f.translateCheck:GetChecked() then + if text == SFrames.Whisper.lastTranslation then + SendChatMessage(text, "WHISPER", nil, SFrames.Whisper.activeContact) + editBox:SetText("") + editBox:ClearFocus() + SFrames.Whisper.lastTranslation = nil + else + local targetLang = "en" + if not string.find(text, "[\128-\255]") then + targetLang = "zh" + end + + if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then + SFrames.Whisper.isTranslating = true + editBox:SetText("翻译中...") + _G.STranslateAPI.Translate(text, "auto", targetLang, function(result, err, meta) + SFrames.Whisper.isTranslating = false + if result then + editBox:SetText(result) + SFrames.Whisper.lastTranslation = result + editBox:SetFocus() + else + editBox:SetText(text) + DEFAULT_CHAT_FRAME:AddMessage("|cffff3333[私聊翻译失败]|r " .. tostring(err)) + end + end, "Nanami-UI") + else + DEFAULT_CHAT_FRAME:AddMessage("|cffff3333[Nanami-UI] STranslate插件未加载|r") + SFrames.Whisper.lastTranslation = text + end + end + else + SendChatMessage(text, "WHISPER", nil, SFrames.Whisper.activeContact) + editBox:SetText("") + editBox:ClearFocus() + end + end + + editBox:SetScript("OnEnterPressed", SendReply) + editBox:SetScript("OnEscapePressed", function() this:ClearFocus() end) + f.editBox = editBox + + local sendBtn = CreateFrame("Button", nil, f) + sendBtn:SetWidth(60) + sendBtn:SetHeight(24) + sendBtn:SetPoint("TOPRIGHT", editBox, "BOTTOMRIGHT", 0, -5) + StyleActionButton(sendBtn, "发送") + sendBtn:SetScript("OnClick", SendReply) + + f:Hide() + self.frame = f +end + +function SFrames.Whisper:Toggle() + self:EnsureFrame() + if self.frame:IsShown() then + self.frame:Hide() + else + self.frame:Show() + self:UpdateContacts() + if self.contacts[1] and not self.activeContact then + self:SelectContact(self.contacts[1]) + elseif self.activeContact then + self:SelectContact(self.activeContact) + end + end +end + +-- Hook Events +local eventFrame = CreateFrame("Frame") +eventFrame:RegisterEvent("CHAT_MSG_WHISPER") +eventFrame:RegisterEvent("CHAT_MSG_WHISPER_INFORM") +eventFrame:RegisterEvent("PLAYER_LOGIN") +eventFrame:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + SFrames.Whisper:LoadCache() + return + end + + local text = arg1 + local sender = arg2 + if not sender or sender == "" then return end + + sender = string.gsub(sender, "-.*", "") -- remove realm name if attached + + if event == "CHAT_MSG_WHISPER" then + SFrames.Whisper:AddMessage(sender, text, false) + elseif event == "CHAT_MSG_WHISPER_INFORM" then + SFrames.Whisper:AddMessage(sender, text, true) + end +end) diff --git a/WorldMap.lua b/WorldMap.lua new file mode 100644 index 0000000..3cd2b80 --- /dev/null +++ b/WorldMap.lua @@ -0,0 +1,1941 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: WorldMap Module (WorldMap.lua) +-- Full Nanami-UI themed world map: windowed, dark pink-purple skin, +-- hides all Blizzard decorations, custom title bar + close button, +-- brute-force tooltip theming while the map is open. +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.WorldMap = {} +local WM = SFrames.WorldMap + +-------------------------------------------------------------------------------- +-- Nanami theme constants (matches SocialUI / ConfigUI / QuestUI) +-------------------------------------------------------------------------------- +local PANEL_BACKDROP = { + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, +} +local _A = SFrames.ActiveTheme + +local TOOLTIP_BACKDROP = { + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, +} + +-------------------------------------------------------------------------------- +-- Config +-------------------------------------------------------------------------------- +function WM:GetConfig() + SFramesDB = SFramesDB or {} + if type(SFramesDB.WorldMap) ~= "table" then + SFramesDB.WorldMap = { enabled = true, scale = 0.85 } + end + if SFramesDB.WorldMap.enabled == nil then + SFramesDB.WorldMap.enabled = true + end + return SFramesDB.WorldMap +end + +-------------------------------------------------------------------------------- +-- HookScript helper +-------------------------------------------------------------------------------- +local function HookScript(frame, script, fn) + local prev = frame:GetScript(script) + frame:SetScript(script, function(a1,a2,a3,a4,a5,a6,a7,a8,a9) + if prev then prev(a1,a2,a3,a4,a5,a6,a7,a8,a9) end + fn(a1,a2,a3,a4,a5,a6,a7,a8,a9) + end) +end + +-------------------------------------------------------------------------------- +-- 1. Hide Blizzard Decorations +-- Called at init AND on every WorldMapFrame:OnShow to counter Blizzard resets +-------------------------------------------------------------------------------- +local function NukeFrame(f) + if not f then return end + f:Hide() + f:SetAlpha(0) + f:EnableMouse(false) + f:ClearAllPoints() + f:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -9999, 9999) + f:SetWidth(1) + f:SetHeight(1) + if not f._nanamiNuked then + f._nanamiNuked = true + f.Show = function(self) self:Hide() end + if f.SetScript then + f:SetScript("OnShow", function() this:Hide() end) + end + end +end + +local function HideAllRegions(frame) + if not frame or not frame.GetRegions then return end + local regions = { frame:GetRegions() } + for _, r in ipairs(regions) do + if r and r.Hide then r:Hide() end + end +end + +local blizzHooked = false + +local function NukeBorderChildren(border) + if not border or not border.GetChildren then return end + local children = { border:GetChildren() } + for _, child in ipairs(children) do + local name = child.GetName and child:GetName() or "" + local isDropDown = name ~= "" and string.find(name, "DropDown") + if not isDropDown then + NukeFrame(child) + end + end +end + +local function HideBlizzardDecorations() + if BlackoutWorld then BlackoutWorld:Hide() end + + local _G = getfenv(0) + + local framesToNuke = { + "WorldMapFrameCloseButton", + "WorldMapFrameSizeUpButton", + "WorldMapFrameSizeDownButton", + } + for _, name in ipairs(framesToNuke) do + NukeFrame(_G[name]) + end + + HideAllRegions(WorldMapFrame) + + local borderNames = { + "WorldMapFrameMiniBorderLeft", + "WorldMapFrameMiniBorderRight", + } + for _, name in ipairs(borderNames) do + local border = _G[name] + if border then + HideAllRegions(border) + NukeBorderChildren(border) + border:EnableMouse(false) + + if not blizzHooked then + local oldShow = border.Show + if oldShow then + border.Show = function(self) + oldShow(self) + HideAllRegions(self) + NukeBorderChildren(self) + self:EnableMouse(false) + end + end + end + end + end + + if WorldMapFrameTitle then + WorldMapFrameTitle:Hide() + end + + blizzHooked = true +end + +-------------------------------------------------------------------------------- +-- 2. Create Nanami Skin +-------------------------------------------------------------------------------- +local skinFrame, titleBar, titleText, closeBtn, coordText, mouseCoordText, coordOverlay + +local function CreateNanamiSkin() + if skinFrame then return end + + local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + + skinFrame = CreateFrame("Frame", "NanamiWorldMapSkin", WorldMapFrame) + skinFrame:SetFrameLevel(WorldMapFrame:GetFrameLevel()) + skinFrame:SetAllPoints(WorldMapFrame) + skinFrame:SetBackdrop(PANEL_BACKDROP) + skinFrame:SetBackdropColor(unpack(_A.panelBg)) + skinFrame:SetBackdropBorderColor(unpack(_A.panelBorder)) + + titleBar = CreateFrame("Frame", nil, skinFrame) + titleBar:SetHeight(26) + titleBar:SetPoint("TOPLEFT", skinFrame, "TOPLEFT", 4, -4) + titleBar:SetPoint("TOPRIGHT", skinFrame, "TOPRIGHT", -4, -4) + titleBar:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + titleBar:SetBackdropColor(unpack(_A.headerBg)) + titleBar:SetBackdropBorderColor(unpack(_A.panelBorder)) + + local titleIco = SFrames:CreateIcon(titleBar, "worldmap", 14) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("LEFT", titleBar, "LEFT", 10, 0) + titleIco:SetVertexColor(unpack(_A.title)) + + titleText = titleBar:CreateFontString(nil, "OVERLAY") + titleText:SetFont(font, 13, "OUTLINE") + titleText:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) + titleText:SetTextColor(unpack(_A.title)) + titleText:SetText("世界地图") + + local hintText = titleBar:CreateFontString(nil, "OVERLAY") + hintText:SetFont(font, 10, "") + hintText:SetPoint("RIGHT", titleBar, "RIGHT", -40, 0) + hintText:SetTextColor(unpack(_A.dimText)) + hintText:SetText("Ctrl+滚轮缩放 | Shift+滚轮透明度 | Ctrl+左键标记") + + closeBtn = CreateFrame("Button", nil, titleBar) + closeBtn:SetWidth(22) + closeBtn:SetHeight(22) + closeBtn:SetPoint("RIGHT", titleBar, "RIGHT", -2, 0) + closeBtn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + closeBtn:SetBackdropColor(unpack(_A.buttonDownBg)) + closeBtn:SetBackdropBorderColor(unpack(_A.btnBorder)) + + local closeIcon = closeBtn:CreateTexture(nil, "OVERLAY") + closeIcon:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") + closeIcon:SetTexCoord(0.25, 0.375, 0, 0.125) + closeIcon:SetPoint("CENTER", 0, 0) + closeIcon:SetWidth(14) + closeIcon:SetHeight(14) + + closeBtn:SetScript("OnClick", function() + WorldMapFrame:Hide() + end) + closeBtn:SetScript("OnEnter", function() + this:SetBackdropColor(unpack(_A.btnHoverBg)) + this:SetBackdropBorderColor(unpack(_A.btnHoverBd)) + end) + closeBtn:SetScript("OnLeave", function() + this:SetBackdropColor(unpack(_A.buttonDownBg)) + this:SetBackdropBorderColor(unpack(_A.btnBorder)) + end) + + -- High-level overlay for coordinates so they render above map content + coordOverlay = CreateFrame("Frame", "NanamiWorldMapCoordOverlay", WorldMapFrame) + coordOverlay:SetAllPoints(skinFrame) + coordOverlay:SetFrameStrata("TOOLTIP") + coordOverlay:SetFrameLevel(250) + coordOverlay:EnableMouse(false) + + coordText = coordOverlay:CreateFontString(nil, "OVERLAY") + coordText:SetFont(font, 12, "OUTLINE") + coordText:SetPoint("BOTTOMLEFT", skinFrame, "BOTTOMLEFT", 12, 6) + coordText:SetTextColor(unpack(_A.title)) + coordText:SetText("") + + mouseCoordText = coordOverlay:CreateFontString(nil, "OVERLAY") + mouseCoordText:SetFont(font, 12, "OUTLINE") + mouseCoordText:SetPoint("BOTTOMRIGHT", skinFrame, "BOTTOMRIGHT", -12, 6) + mouseCoordText:SetTextColor(unpack(_A.dimText)) + mouseCoordText:SetText("") +end + +local function UpdateTitle() + if not titleText then return end + local zoneName = GetZoneText and GetZoneText() or "" + local subZone = GetSubZoneText and GetSubZoneText() or "" + local display = zoneName + if subZone ~= "" and subZone ~= zoneName then + display = zoneName .. " - " .. subZone + end + if display == "" then display = "世界地图" end + titleText:SetText(display) +end + +local function UpdateCoords() + if not coordText then return end + local x, y = GetPlayerMapPosition("player") + if x and y and (x > 0 or y > 0) then + coordText:SetText(string.format("玩家: %.1f, %.1f", x * 100, y * 100)) + else + coordText:SetText("") + end +end + +local function UpdateMouseCoords() + if not mouseCoordText or not WorldMapButton then return end + if not WorldMapFrame or not WorldMapFrame:IsVisible() then + mouseCoordText:SetText("") + return + end + + local cx, cy = GetCursorPosition() + if not cx or not cy then + mouseCoordText:SetText("") + return + end + + local scale = WorldMapButton:GetEffectiveScale() + local left = WorldMapButton:GetLeft() + local top = WorldMapButton:GetTop() + local w = WorldMapButton:GetWidth() + local h = WorldMapButton:GetHeight() + + if not left or not top or not w or not h or w == 0 or h == 0 then + mouseCoordText:SetText("") + return + end + + local mx = (cx / scale - left) / w + local my = (top - cy / scale) / h + + if mx >= 0 and mx <= 1 and my >= 0 and my <= 1 then + mouseCoordText:SetText(string.format("鼠标: %.1f, %.1f", mx * 100, my * 100)) + else + mouseCoordText:SetText("") + end +end + +-------------------------------------------------------------------------------- +-- 3. Tooltip theming +-- SetBackdrop on scaled child frames is unreliable in WoW 1.12. +-- Approach: use a SEPARATE frame parented to UIParent (outside the +-- WorldMapFrame scale chain) positioned exactly behind whichever tooltip +-- is currently visible. This frame has its own backdrop at TOOLTIP strata. +-- Also hook WorldMapTooltip:Show to try SetBackdrop as secondary attempt. +-------------------------------------------------------------------------------- +local ttBG + +local TT_PAD = 4 + +local function CreateTooltipBG() + if ttBG then return end + ttBG = CreateFrame("Frame", "NanamiMapTTBG", UIParent) + ttBG:SetFrameStrata("FULLSCREEN_DIALOG") + ttBG:SetFrameLevel(255) + ttBG:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + ttBG:SetBackdropColor(unpack(_A.panelBg)) + ttBG:SetBackdropBorderColor(unpack(_A.panelBorder)) + ttBG:Hide() +end + +local function PositionTTBG(tip) + if not ttBG then return end + if not tip or not tip:IsVisible() then + ttBG:Hide() + return + end + + local l = tip:GetLeft() + local r = tip:GetRight() + local t = tip:GetTop() + local b = tip:GetBottom() + if not l or not r or not t or not b then + ttBG:Hide() + return + end + + local tipScale = tip:GetEffectiveScale() + local bgScale = ttBG:GetEffectiveScale() + local s = tipScale / bgScale + + ttBG:ClearAllPoints() + ttBG:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", l * s - TT_PAD, t * s + TT_PAD) + ttBG:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMLEFT", r * s + TT_PAD, b * s - TT_PAD) + ttBG:Show() +end + +local function HideTTBG() + if ttBG then ttBG:Hide() end +end + +local function RaiseTooltipStrata(tip) + if not tip then return end + tip:SetFrameStrata("TOOLTIP") +end + +local function HookTooltipShow(tip) + if not tip or tip._nanamiShowHooked then return end + tip._nanamiShowHooked = true + local origShow = tip.Show + tip.Show = function(self) + origShow(self) + if WorldMapFrame and WorldMapFrame:IsVisible() then + self:SetFrameStrata("TOOLTIP") + self:SetBackdrop(TOOLTIP_BACKDROP) + self:SetBackdropColor(unpack(_A.panelBg)) + self:SetBackdropBorderColor(unpack(_A.panelBorder)) + end + end +end + +-------------------------------------------------------------------------------- +-- 4. Per-frame updater +-------------------------------------------------------------------------------- +local elapsed = 0 +local function OnFrameUpdate() + if not WorldMapFrame or not WorldMapFrame:IsShown() then + HideTTBG() + return + end + + local activeTip = nil + if WorldMapTooltip and WorldMapTooltip:IsVisible() then + activeTip = WorldMapTooltip + elseif GameTooltip and GameTooltip:IsVisible() then + activeTip = GameTooltip + end + + if activeTip then + PositionTTBG(activeTip) + RaiseTooltipStrata(activeTip) + activeTip:SetBackdrop(TOOLTIP_BACKDROP) + activeTip:SetBackdropColor(unpack(_A.panelBg)) + activeTip:SetBackdropBorderColor(unpack(_A.panelBorder)) + else + HideTTBG() + end + + UpdateMouseCoords() + + elapsed = elapsed + (arg1 or 0) + if elapsed > 0.1 then + elapsed = 0 + UpdateTitle() + UpdateCoords() + HideBlizzardDecorations() + end +end + +-------------------------------------------------------------------------------- +-- 5. Layout +-------------------------------------------------------------------------------- +local function ApplyLayout(cfg) + local s = cfg.scale or 0.85 + WorldMapFrame:SetMovable(true) + WorldMapFrame:EnableMouse(true) + WorldMapFrame:SetScale(s) + WorldMapFrame:ClearAllPoints() + WorldMapFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 30) + WorldMapFrame:SetWidth(WorldMapButton:GetWidth() + 20) + WorldMapFrame:SetHeight(WorldMapButton:GetHeight() + 60) + if BlackoutWorld then + BlackoutWorld:Hide() + end +end + +-------------------------------------------------------------------------------- +-- 6. Window mode setup +-------------------------------------------------------------------------------- +function WM:SetupWindowMode() + if Cartographer or METAMAP_TITLE then return end + + local cfg = self:GetConfig() + + table.insert(UISpecialFrames, "WorldMapFrame") + + local _G = getfenv(0) + _G.ToggleWorldMap = function() + if WorldMapFrame:IsShown() then + WorldMapFrame:Hide() + else + WorldMapFrame:Show() + end + end + + UIPanelWindows["WorldMapFrame"] = { area = "center" } + + HookScript(WorldMapFrame, "OnShow", function() + this:EnableKeyboard(false) + this:EnableMouseWheel(1) + local c = WM:GetConfig() + WorldMapFrame:SetScale(c.scale or 0.85) + WorldMapFrame:SetAlpha(1) + WorldMapFrame:SetFrameStrata("FULLSCREEN_DIALOG") + if BlackoutWorld then BlackoutWorld:Hide() end + HideBlizzardDecorations() + end) + + HookScript(WorldMapFrame, "OnHide", function() + HideTTBG() + end) + + HookScript(WorldMapFrame, "OnMouseWheel", function() + if IsShiftKeyDown() then + local a = WorldMapFrame:GetAlpha() + arg1 / 10 + if a < 0.2 then a = 0.2 end + if a > 1 then a = 1 end + WorldMapFrame:SetAlpha(a) + elseif IsControlKeyDown() then + local oldScale = WorldMapFrame:GetScale() + local newScale = oldScale + arg1 / 10 + if newScale < 0.4 then newScale = 0.4 end + if newScale > 1.5 then newScale = 1.5 end + + local eff = WorldMapFrame:GetEffectiveScale() + local fw = WorldMapFrame:GetWidth() + local fh = WorldMapFrame:GetHeight() + local fl = WorldMapFrame:GetLeft() + local ft = WorldMapFrame:GetTop() + + if fl and ft and fw and fh and eff and eff > 0 then + local scrCX = (fl + fw / 2) * eff + local scrCY = (ft - fh / 2) * eff + WorldMapFrame:SetScale(newScale) + local newEff = newScale * eff / oldScale + WorldMapFrame:ClearAllPoints() + WorldMapFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", + scrCX / newEff - fw / 2, + scrCY / newEff + fh / 2) + else + WorldMapFrame:SetScale(newScale) + end + + local c = WM:GetConfig() + c.scale = newScale + end + end) + + HookScript(WorldMapFrame, "OnMouseDown", function() + WorldMapFrame:StartMoving() + end) + + HookScript(WorldMapFrame, "OnMouseUp", function() + WorldMapFrame:StopMovingOrSizing() + end) + + ApplyLayout(cfg) + + local _G2 = getfenv(0) + local origMaximize = _G2.WorldMapFrame_Maximize + local origMinimize = _G2.WorldMapFrame_Minimize + _G2.WorldMapFrame_Maximize = function() + if origMaximize then origMaximize() end + ApplyLayout(WM:GetConfig()) + HideBlizzardDecorations() + end + _G2.WorldMapFrame_Minimize = function() + if origMinimize then origMinimize() end + ApplyLayout(WM:GetConfig()) + HideBlizzardDecorations() + end +end + +-------------------------------------------------------------------------------- +-- 7. Skin World Map Native Controls (Dropdowns, Buttons) +-------------------------------------------------------------------------------- + +local function SkinDropDown(dropdownName) + local _G = getfenv(0) + local dd = _G[dropdownName] + if not dd or dd._nanamiSkinned then return end + dd._nanamiSkinned = true + + for _, suffix in ipairs({ "Left", "Middle", "Right" }) do + local tex = _G[dropdownName .. suffix] + if tex then tex:SetAlpha(0) end + end + + local bg = CreateFrame("Frame", nil, dd) + bg:SetPoint("TOPLEFT", dd, "TOPLEFT", 16, -2) + bg:SetPoint("BOTTOMRIGHT", dd, "BOTTOMRIGHT", -16, 6) + bg:SetFrameLevel(dd:GetFrameLevel()) + bg:SetBackdrop(PANEL_BACKDROP) + bg:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4]) + bg:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4]) + dd._nanamiBG = bg + + local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + + local text = _G[dropdownName .. "Text"] + if text then + text:SetFont(font, 11, "OUTLINE") + text:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3]) + end + + local btn = _G[dropdownName .. "Button"] + if btn then + local nt = btn:GetNormalTexture() + if nt then nt:SetVertexColor(_A.title[1], _A.title[2], _A.title[3]) end + local pt = btn:GetPushedTexture() + if pt then pt:SetVertexColor(_A.title[1], _A.title[2], _A.title[3]) end + local dt = btn:GetDisabledTexture() + if dt then dt:SetVertexColor(_A.dimText[1], _A.dimText[2], _A.dimText[3]) end + local ht = btn:GetHighlightTexture() + if ht then ht:SetVertexColor(_A.title[1], _A.title[2], _A.title[3], 0.3) end + end +end + +local function SkinDropDownLists() + local _G = getfenv(0) + for i = 1, 3 do + local listName = "DropDownList" .. i + local backdrop = _G[listName .. "Backdrop"] + if backdrop then + backdrop:SetBackdrop(PANEL_BACKDROP) + backdrop:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4]) + backdrop:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4]) + end + local menuBackdrop = _G[listName .. "MenuBackdrop"] + if menuBackdrop then + menuBackdrop:SetBackdrop(PANEL_BACKDROP) + menuBackdrop:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4]) + menuBackdrop:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4]) + end + for j = 1, 30 do + local hlTex = _G[listName .. "Button" .. j .. "Highlight"] + if hlTex and hlTex.SetVertexColor then + hlTex:SetVertexColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], 0.35) + end + end + end +end + +local function SkinNativeButton(btnName, labelOverride) + local _G = getfenv(0) + local btn = _G[btnName] + if not btn or btn._nanamiSkinned then return end + btn._nanamiSkinned = true + + local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + + local regions = { btn:GetRegions() } + for _, r in ipairs(regions) do + if r and r.GetObjectType and r:GetObjectType() == "Texture" then + local tex = r:GetTexture() + if tex and type(tex) == "string" and (string.find(tex, "UI%-Panel") or string.find(tex, "UI%-DialogBox")) then + r:Hide() + end + end + end + + local nt = btn.GetNormalTexture and btn:GetNormalTexture() + if nt then nt:SetAlpha(0) end + local pt = btn.GetPushedTexture and btn:GetPushedTexture() + if pt then pt:SetAlpha(0) end + local ht = btn.GetHighlightTexture and btn:GetHighlightTexture() + if ht then ht:SetAlpha(0) end + + btn:SetBackdrop(PANEL_BACKDROP) + btn:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4]) + btn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4]) + + if labelOverride then + local fs = btn:GetFontString() or btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(font, 11, "OUTLINE") + fs:SetText(labelOverride) + fs:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3]) + fs:SetPoint("CENTER", 0, 0) + elseif btn.GetFontString and btn:GetFontString() then + local fs = btn:GetFontString() + fs:SetFont(font, 11, "OUTLINE") + fs:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3]) + end + + local prevEnter = btn:GetScript("OnEnter") + local prevLeave = btn:GetScript("OnLeave") + btn:SetScript("OnEnter", function() + if prevEnter then prevEnter() end + this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4]) + this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4]) + end) + btn:SetScript("OnLeave", function() + if prevLeave then prevLeave() end + this:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4]) + this:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4]) + end) +end + +local ddListHooked = false +local function SkinWorldMapControls() + SkinDropDown("WorldMapContinentDropDown") + SkinDropDown("WorldMapZoneDropDown") + SkinDropDown("WorldMapZoneMinimapDropDown") + + SkinNativeButton("WorldMapZoomOutButton", "缩小") + + SkinDropDownLists() + + if not ddListHooked then + ddListHooked = true + local _G = getfenv(0) + for i = 1, 3 do + local list = _G["DropDownList" .. i] + if list then + local oldShow = list.Show + if oldShow then + list.Show = function(self) + oldShow(self) + SkinDropDownLists() + end + end + end + end + end +end + +-------------------------------------------------------------------------------- +-- 8. Waypoint / Map Pin System +-------------------------------------------------------------------------------- +local waypoint = { active = false, continent = 0, zone = 0, x = 0, y = 0, name = "" } +local pinFrame, pinTexture, pinLabel, pinShareBtn, pinClearBtn + +local function GetMapCursorPos() + if not WorldMapButton then return nil, nil end + local cx, cy = GetCursorPosition() + if not cx or not cy then return nil, nil end + local scale = WorldMapButton:GetEffectiveScale() + local left = WorldMapButton:GetLeft() + local top = WorldMapButton:GetTop() + local w = WorldMapButton:GetWidth() + local h = WorldMapButton:GetHeight() + if not left or not top or not w or not h or w == 0 or h == 0 then return nil, nil end + local mx = (cx / scale - left) / w + local my = (top - cy / scale) / h + if mx >= 0 and mx <= 1 and my >= 0 and my <= 1 then + return mx, my + end + return nil, nil +end + +local function GetCurrentMapName() + local _G = getfenv(0) + local zoneText = _G["WorldMapFrameAreaLabel"] + if zoneText and zoneText.GetText then + local t = zoneText:GetText() + if t and t ~= "" then return t end + end + local c = GetCurrentMapContinent and GetCurrentMapContinent() or 0 + local z = GetCurrentMapZone and GetCurrentMapZone() or 0 + if z > 0 and GetMapZones then + local zones = { GetMapZones(c) } + if zones[z] then return zones[z] end + end + if c > 0 and GetMapContinents then + local continents = { GetMapContinents() } + if continents[c] then return continents[c] end + end + return "未知区域" +end + +local function CreateWaypointPin() + if pinFrame then return end + local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + + pinFrame = CreateFrame("Frame", "NanamiWorldMapPin", WorldMapButton) + pinFrame:SetWidth(24) + pinFrame:SetHeight(38) + pinFrame:SetFrameStrata("FULLSCREEN_DIALOG") + pinFrame:SetFrameLevel(200) + + local stem = pinFrame:CreateTexture(nil, "ARTWORK") + stem:SetTexture("Interface\\Buttons\\WHITE8X8") + stem:SetWidth(2) + stem:SetHeight(14) + stem:SetPoint("BOTTOM", pinFrame, "BOTTOM", 0, 0) + stem:SetVertexColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], 0.85) + + local glow = pinFrame:CreateTexture(nil, "BACKGROUND") + glow:SetTexture("Interface\\Buttons\\WHITE8X8") + glow:SetWidth(20) + glow:SetHeight(20) + glow:SetPoint("BOTTOM", stem, "TOP", 0, -6) + glow:SetVertexColor(_A.accent[1], _A.accent[2], _A.accent[3], 0.30) + + pinTexture = pinFrame:CreateTexture(nil, "ARTWORK") + pinTexture:SetTexture("Interface\\Buttons\\WHITE8X8") + pinTexture:SetWidth(12) + pinTexture:SetHeight(12) + pinTexture:SetPoint("BOTTOM", stem, "TOP", 0, -1) + pinTexture:SetVertexColor(_A.accent[1], _A.accent[2], _A.accent[3], 1) + + local dot = pinFrame:CreateTexture(nil, "OVERLAY") + dot:SetTexture("Interface\\Buttons\\WHITE8X8") + dot:SetWidth(4) + dot:SetHeight(4) + dot:SetPoint("CENTER", pinTexture, "CENTER", 0, 0) + dot:SetVertexColor(_A.title[1], _A.title[2], _A.title[3], 1) + + pinLabel = pinFrame:CreateFontString(nil, "OVERLAY") + pinLabel:SetFont(font, 11, "OUTLINE") + pinLabel:SetPoint("TOP", pinFrame, "BOTTOM", 0, -2) + pinLabel:SetTextColor(_A.title[1], _A.title[2], _A.title[3]) + pinLabel:SetText("") + + local btnW, btnH = 52, 18 + + pinShareBtn = CreateFrame("Button", nil, pinFrame) + pinShareBtn:SetWidth(btnW) + pinShareBtn:SetHeight(btnH) + pinShareBtn:SetPoint("TOPLEFT", pinFrame, "BOTTOMLEFT", -10, -8) + pinShareBtn:SetBackdrop(PANEL_BACKDROP) + pinShareBtn:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4]) + pinShareBtn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4]) + local shareFS = pinShareBtn:CreateFontString(nil, "OVERLAY") + shareFS:SetFont(font, 10, "OUTLINE") + shareFS:SetPoint("CENTER", 0, 0) + shareFS:SetText("分享") + shareFS:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3]) + pinShareBtn:SetScript("OnEnter", function() + this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4]) + end) + pinShareBtn:SetScript("OnLeave", function() + this:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4]) + end) + pinShareBtn:SetScript("OnClick", function() + WM:ShareWaypoint() + end) + + pinClearBtn = CreateFrame("Button", nil, pinFrame) + pinClearBtn:SetWidth(btnW) + pinClearBtn:SetHeight(btnH) + pinClearBtn:SetPoint("LEFT", pinShareBtn, "RIGHT", 4, 0) + pinClearBtn:SetBackdrop(PANEL_BACKDROP) + pinClearBtn:SetBackdropColor(_A.buttonDownBg[1], _A.buttonDownBg[2], _A.buttonDownBg[3], _A.buttonDownBg[4]) + pinClearBtn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4]) + local clearFS = pinClearBtn:CreateFontString(nil, "OVERLAY") + clearFS:SetFont(font, 10, "OUTLINE") + clearFS:SetPoint("CENTER", 0, 0) + clearFS:SetText("清除") + clearFS:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3]) + pinClearBtn:SetScript("OnEnter", function() + this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4]) + end) + pinClearBtn:SetScript("OnLeave", function() + this:SetBackdropColor(_A.buttonDownBg[1], _A.buttonDownBg[2], _A.buttonDownBg[3], _A.buttonDownBg[4]) + end) + pinClearBtn:SetScript("OnClick", function() + WM:ClearWaypoint() + end) + + pinFrame:Hide() +end + +local function UpdatePinPosition() + if not pinFrame or not waypoint.active then + if pinFrame then pinFrame:Hide() end + return + end + if not WorldMapButton or not WorldMapFrame:IsVisible() then + pinFrame:Hide() + return + end + local c = GetCurrentMapContinent and GetCurrentMapContinent() or 0 + local z = GetCurrentMapZone and GetCurrentMapZone() or 0 + if c ~= waypoint.continent or z ~= waypoint.zone then + pinFrame:Hide() + return + end + local w = WorldMapButton:GetWidth() + local h = WorldMapButton:GetHeight() + if not w or not h or w == 0 or h == 0 then + pinFrame:Hide() + return + end + pinFrame:ClearAllPoints() + pinFrame:SetPoint("BOTTOM", WorldMapButton, "TOPLEFT", w * waypoint.x, -h * waypoint.y) + pinFrame:Show() +end + +function WM:SetWaypoint(continent, zone, x, y, name) + waypoint.active = true + waypoint.continent = continent + waypoint.zone = zone + waypoint.x = x + waypoint.y = y + waypoint.name = name or "标记点" + + CreateWaypointPin() + local xPct = string.format("%.1f", x * 100) + local yPct = string.format("%.1f", y * 100) + pinLabel:SetText(waypoint.name .. " (" .. xPct .. ", " .. yPct .. ")") + UpdatePinPosition() + + if SFrames and SFrames.Print then + SFrames:Print("地图标记已放置: " .. waypoint.name .. " (" .. xPct .. ", " .. yPct .. ")") + end +end + +function WM:ClearWaypoint() + waypoint.active = false + waypoint.continent = 0 + waypoint.zone = 0 + waypoint.x = 0 + waypoint.y = 0 + waypoint.name = "" + if pinFrame then pinFrame:Hide() end +end + +function WM:ShareWaypoint() + if not waypoint.active then return end + local xPct = string.format("%.1f", waypoint.x * 100) + local yPct = string.format("%.1f", waypoint.y * 100) + local shareText = "" + if ChatFrameEditBox then + if not ChatFrameEditBox:IsVisible() then + ChatFrameEditBox:Show() + end + ChatFrameEditBox:SetFocus() + ChatFrameEditBox:SetText(shareText .. " " .. waypoint.name .. " (" .. xPct .. ", " .. yPct .. ")") + end +end + +function WM:HandleWaypointLink(data) + local _, _, cs, zs, xs, ys = string.find(data, "^(%d+):(%d+):([%d%.]+):([%d%.]+)") + if not cs then return end + local c = tonumber(cs) or 0 + local z = tonumber(zs) or 0 + local x = (tonumber(xs) or 0) / 100 + local y = (tonumber(ys) or 0) / 100 + + local name = "标记点" + if z > 0 and GetMapZones then + local zones = { GetMapZones(c) } + if zones[z] then name = zones[z] end + elseif c > 0 and GetMapContinents then + local continents = { GetMapContinents() } + if continents[c] then name = continents[c] end + end + + if not WorldMapFrame:IsVisible() then + WorldMapFrame:Show() + end + + self:SetWaypoint(c, z, x, y, name) + + local pending = { continent = c, zone = z, x = x, y = y, name = name } + local timer = CreateFrame("Frame") + local waited = 0 + timer:SetScript("OnUpdate", function() + waited = waited + (arg1 or 0) + if waited >= 0.05 then + timer:SetScript("OnUpdate", nil) + if SetMapZoom then + SetMapZoom(pending.continent, pending.zone) + end + WM:SetWaypoint(pending.continent, pending.zone, pending.x, pending.y, pending.name) + end + end) +end + +-------------------------------------------------------------------------------- +-- 8b. Darkmoon Faire Map Markers (暗月马戏团世界地图标记) +-------------------------------------------------------------------------------- +local DMF_ICON = "Interface\\Icons\\INV_Misc_Orb_02" +local DMF_SIZE = 24 +local DMF_SIZE_CONT = 16 +local DMF_LOCATIONS = { + { zone = "ElwynnForest", cont = 2, + zx = 0.41, zy = 0.69, -- zone-level coordinates + cx = 0.453, cy = 0.721, -- continent-level coordinates (Eastern Kingdoms) + name = "闪金镇", fullName = "闪金镇 (艾尔文森林)", + }, + { zone = "Mulgore", cont = 1, + zx = 0.36, zy = 0.38, -- zone-level coordinates + cx = 0.463, cy = 0.595, -- continent-level coordinates (Kalimdor) + name = "莫高雷", fullName = "莫高雷", + }, +} +local DMF_REF_LOC = 1 -- ref Monday (2026-03-09) week → Goldshire +local dmfPins = {} +local dmfPulse = 0 +local dmfRefJDN = nil + +local function DiscoverDmfZoneIndices() + local savedC = GetCurrentMapContinent and GetCurrentMapContinent() or 0 + local savedZ = GetCurrentMapZone and GetCurrentMapZone() or 0 + for i = 1, 2 do + local loc = DMF_LOCATIONS[i] + if not loc.zoneIdx then + local target = string.lower(loc.zone) + local zones = { GetMapZones(loc.cont) } + for idx = 1, table.getn(zones) do + SetMapZoom(loc.cont, idx) + local mf = GetMapInfo and GetMapInfo() or "" + if mf ~= "" then + if mf == loc.zone then + loc.zoneIdx = idx + break + elseif string.find(string.lower(mf), target) then + loc.zoneIdx = idx + loc.zone = mf + break + elseif string.find(target, string.lower(mf)) then + loc.zoneIdx = idx + loc.zone = mf + break + end + end + end + end + end + if savedZ > 0 then + SetMapZoom(savedC, savedZ) + elseif savedC > 0 then + SetMapZoom(savedC, 0) + else + if SetMapToCurrentZone then SetMapToCurrentZone() end + end +end + +local function DmfJDN(y, m, d) + local a = math.floor((14 - m) / 12) + local y2 = y + 4800 - a + local m2 = m + 12 * a - 3 + return d + math.floor((153 * m2 + 2) / 5) + 365 * y2 + + math.floor(y2 / 4) - math.floor(y2 / 100) + math.floor(y2 / 400) - 32045 +end + +local function GetCETJDN() + local ts = time() + local daysSinceEpoch = math.floor(ts / 86400) + local utcJDN = 2440588 + daysSinceEpoch -- Jan 1 1970 = JDN 2440588 + local utcHour = math.mod(math.floor(ts / 3600), 24) + + local ok, utc = pcall(date, "!*t") + local utcYear + if ok and utc and utc.year then + utcYear = utc.year + else + local lt = date("*t") + utcYear = lt.year + end + + local mar31dow = math.mod(DmfJDN(utcYear, 3, 31), 7) + local lastSunMar = 31 - math.mod(mar31dow + 1, 7) + local oct31dow = math.mod(DmfJDN(utcYear, 10, 31), 7) + local lastSunOct = 31 - math.mod(oct31dow + 1, 7) + + local utcMonth = math.floor((daysSinceEpoch - 10957) / 30.44) + 1 + if ok and utc and utc.month then utcMonth = utc.month end + local utcDay = 0 + if ok and utc then utcDay = utc.day or 0 end + + local afterSpring = (utcMonth > 3) + or (utcMonth == 3 and utcDay > lastSunMar) + or (utcMonth == 3 and utcDay == lastSunMar and utcHour >= 1) + local beforeAutumn = (utcMonth < 10) + or (utcMonth == 10 and utcDay < lastSunOct) + or (utcMonth == 10 and utcDay == lastSunOct and utcHour < 1) + + local offset = (afterSpring and beforeAutumn) and 2 or 1 -- CEST=2, CET=1 + local cetJDN = utcJDN + if utcHour + offset >= 24 then cetJDN = cetJDN + 1 end + return cetJDN +end + +local function GetDmfSchedule() + local todayJDN = GetCETJDN() + local dow = math.mod(todayJDN, 7) -- 0=Mon 1=Tue 2=Wed 3=Thu 4=Fri 5=Sat 6=Sun + local isWed = (dow == 2) + + -- Week runs Sun(6)-Sat(5); ref Sunday March 8 = Goldshire + if not dmfRefJDN then dmfRefJDN = DmfJDN(2026, 3, 8) end + local daysSinceSun = math.mod(dow + 1, 7) -- Sun=0 Mon=1 Tue=2 Wed=3 Thu=4 Fri=5 Sat=6 + local thisSunJDN = todayJDN - daysSinceSun + local weeksDiff = math.floor((thisSunJDN - dmfRefJDN) / 7) + if weeksDiff < 0 then weeksDiff = -weeksDiff end + local ai = math.mod(weeksDiff, 2) == 0 and DMF_REF_LOC or (3 - DMF_REF_LOC) + + local daysLeft = 6 - daysSinceSun -- days until Saturday (0 on Sat itself) + local daysUntilNext = daysLeft + 1 -- days until next Sunday + + return ai, isWed, daysLeft, daysUntilNext +end + +local function CreateDmfPin(i) + if dmfPins[i] then return end + if not WorldMapButton then return end + local f = CreateFrame("Button", "NanamiDMFPin" .. i, WorldMapButton) + f:SetWidth(DMF_SIZE); f:SetHeight(DMF_SIZE) + f:SetFrameStrata("FULLSCREEN_DIALOG") + f:SetFrameLevel(200) + + local ico = f:CreateTexture(nil, "OVERLAY") + ico:SetTexture(DMF_ICON) + ico:SetWidth(DMF_SIZE); ico:SetHeight(DMF_SIZE) + ico:SetPoint("CENTER", f, "CENTER", 0, 0) + f.icon = ico + + local gl = f:CreateTexture(nil, "OVERLAY") + gl:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + gl:SetBlendMode("ADD") + gl:SetWidth(DMF_SIZE * 2.2); gl:SetHeight(DMF_SIZE * 2.2) + gl:SetPoint("CENTER", f, "CENTER", 0, 0) + local _glc = SFrames.ActiveTheme and SFrames.ActiveTheme.accentLight or { 1, 0.84, 0 } + gl:SetVertexColor(_glc[1], _glc[2], _glc[3], 0.35) + f.glow = gl + + f.locIdx = i + f:SetScript("OnEnter", function() + local loc = DMF_LOCATIONS[this.locIdx] + local other = DMF_LOCATIONS[3 - this.locIdx] + local ai, iw, dl, ds = GetDmfSchedule() + local here = (ai == this.locIdx) + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:AddLine("暗月马戏团", 1, 0.84, 0) + GameTooltip:AddLine(loc.fullName, 0.7, 0.7, 0.7) + GameTooltip:AddLine(" ") + if here then + if iw then + GameTooltip:AddLine("今天是马戏团休息日 (周三)", 1, 0.3, 0.3) + GameTooltip:AddLine("马戏团在此处,明天恢复营业", 0.4, 1, 0.4) + GameTooltip:AddLine(" ") + GameTooltip:AddLine("本轮还剩 " .. dl .. " 天 (周六结束)", 0.8, 0.8, 0.8) + else + GameTooltip:AddLine("马戏团正在此处营业中!", 0.4, 1, 0.4) + GameTooltip:AddLine(" ") + if dl > 1 then + GameTooltip:AddLine("剩余 " .. dl .. " 天 (周六结束)", 0.8, 0.8, 0.8) + elseif dl == 1 then + GameTooltip:AddLine("明天是本轮最后一天", 1, 0.84, 0) + else + GameTooltip:AddLine("今天是本轮最后一天!", 1, 0.84, 0) + GameTooltip:AddLine("之后将转移至 " .. other.fullName, 0.6, 0.8, 1) + end + end + else + GameTooltip:AddLine("马戏团目前不在此处", 0.6, 0.6, 0.6) + GameTooltip:AddLine(" ") + GameTooltip:AddLine("当前位置: " .. other.fullName, 0.6, 0.8, 1) + if iw then + GameTooltip:AddLine("今天对方休息," .. ds .. " 天后来此", 1, 0.84, 0) + else + GameTooltip:AddLine(ds .. " 天后到来", 1, 0.84, 0) + end + end + GameTooltip:AddLine(" ") + GameTooltip:AddLine("点击打开Buff指引", 0.5, 0.5, 0.5) + GameTooltip:Show() + end) + f:SetScript("OnLeave", function() GameTooltip:Hide() end) + f:SetScript("OnClick", function() + if SFrames.DarkmoonGuide and SFrames.DarkmoonGuide.Toggle then + SFrames.DarkmoonGuide:Toggle() + end + end) + f:RegisterForClicks("LeftButtonUp") + f:Hide() + dmfPins[i] = f +end + +local function UpdateDarkmoonPins() + if not WorldMapButton or not WorldMapFrame:IsVisible() then + for i = 1, 2 do if dmfPins[i] then dmfPins[i]:Hide() end end + return + end + + local mapFile = GetMapInfo and GetMapInfo() or "" + local curC = GetCurrentMapContinent and GetCurrentMapContinent() or 0 + local curZ = GetCurrentMapZone and GetCurrentMapZone() or 0 + local ai, iw = GetDmfSchedule() + local w = WorldMapButton:GetWidth() + local h = WorldMapButton:GetHeight() + if not w or not h or w == 0 or h == 0 then return end + + for i = 1, 2 do + CreateDmfPin(i) + local pin = dmfPins[i] + local loc = DMF_LOCATIONS[i] + local px, py + local show = false + local isContinent = false + + if curZ > 0 and curC == loc.cont and loc.zoneIdx and curZ == loc.zoneIdx then + show = true + px, py = loc.zx, loc.zy + elseif mapFile ~= "" and mapFile == loc.zone then + show = true + px, py = loc.zx, loc.zy + elseif curZ == 0 and curC == loc.cont then + show = true + isContinent = true + px, py = loc.cx, loc.cy + end + + if show then + local sz = isContinent and DMF_SIZE_CONT or DMF_SIZE + pin:SetWidth(sz); pin:SetHeight(sz) + pin.icon:SetWidth(sz); pin.icon:SetHeight(sz) + pin.glow:SetWidth(sz * 2.2); pin.glow:SetHeight(sz * 2.2) + pin:ClearAllPoints() + pin:SetPoint("CENTER", WorldMapButton, "TOPLEFT", w * px, -h * py) + if ai == i then + if iw then + pin.icon:SetVertexColor(0.75, 0.75, 0.75) + pin:SetAlpha(0.85) + pin.glow:Hide() + else + pin.icon:SetVertexColor(1, 1, 1) + pin:SetAlpha(1) + pin.glow:Show() + end + else + pin.icon:SetVertexColor(0.45, 0.45, 0.45) + pin:SetAlpha(0.7) + pin.glow:Hide() + end + pin:Show() + else + pin:Hide() + end + end + + dmfPulse = dmfPulse + 0.03 + if dmfPulse > 6.2832 then dmfPulse = dmfPulse - 6.2832 end + for i = 1, 2 do + local p = dmfPins[i] + if p and p:IsShown() and p.glow and p.glow:IsShown() then + p.glow:SetAlpha(0.25 + 0.20 * math.sin(dmfPulse * 2)) + end + end +end + +SLASH_DMFMAP1 = "/dmfmap" +SlashCmdList["DMFMAP"] = function(msg) + local cf = DEFAULT_CHAT_FRAME + if msg == "scan" then + local savedC = GetCurrentMapContinent and GetCurrentMapContinent() or 0 + local savedZ = GetCurrentMapZone and GetCurrentMapZone() or 0 + for c = 1, 2 do + local cname = c == 1 and "Kalimdor" or "Eastern Kingdoms" + cf:AddMessage("|cffffcc66[DMF Scan] " .. cname .. " (cont=" .. c .. "):|r") + local zones = { GetMapZones(c) } + for idx = 1, table.getn(zones) do + SetMapZoom(c, idx) + local mf = GetMapInfo and GetMapInfo() or "(nil)" + local zname = zones[idx] or "?" + if string.find(string.lower(mf), "elwynn") or string.find(string.lower(zname), "elwynn") + or string.find(string.lower(mf), "mulgore") or string.find(string.lower(zname), "mulgore") then + cf:AddMessage(" |cff00ff00>>|r idx=" .. idx .. " file=" .. tostring(mf) .. " name=" .. tostring(zname)) + else + cf:AddMessage(" idx=" .. idx .. " file=" .. tostring(mf) .. " name=" .. tostring(zname)) + end + end + end + if savedZ > 0 then SetMapZoom(savedC, savedZ) + elseif savedC > 0 then SetMapZoom(savedC, 0) + else if SetMapToCurrentZone then SetMapToCurrentZone() end end + return + end + local ai, iw, dl, ds = GetDmfSchedule() + local n = DMF_LOCATIONS[ai] and DMF_LOCATIONS[ai].name or "?" + local mf = GetMapInfo and GetMapInfo() or "(nil)" + local curC = GetCurrentMapContinent and GetCurrentMapContinent() or 0 + local curZ = GetCurrentMapZone and GetCurrentMapZone() or 0 + local cetJDN = GetCETJDN() + local dow = math.mod(cetJDN, 7) + local dayNames = { [0]="Mon", [1]="Tue", [2]="Wed", [3]="Thu", [4]="Fri", [5]="Sat", [6]="Sun" } + local localT = date("%H:%M") + local ts = time() + local utcH = math.mod(math.floor(ts / 3600), 24) + local utcM = math.mod(math.floor(ts / 60), 60) + cf:AddMessage("|cffffcc66[暗月马戏团]|r 本周: " .. n + .. " | CET " .. (dayNames[dow] or "?") .. (iw and "(休息)" or "") + .. " | 剩余" .. dl .. "天 | " .. ds .. "天后轮换") + cf:AddMessage(" 本地=" .. localT .. " UTC=" .. string.format("%02d:%02d", utcH, utcM) + .. " | map=" .. tostring(mf) + .. " | cont=" .. curC .. " zone=" .. curZ) + for i = 1, 2 do + local p = dmfPins[i] + local loc = DMF_LOCATIONS[i] + cf:AddMessage(" " .. loc.name .. ": " + .. (p and ("shown=" .. tostring(p:IsShown())) or "未创建") + .. " | zoneIdx=" .. tostring(loc.zoneIdx or "未发现") + .. " | file=" .. loc.zone .. " cont=" .. loc.cont) + end +end + +-------------------------------------------------------------------------------- +-- 9. Chat Link Integration: transform and handle nmwp: clicks +-------------------------------------------------------------------------------- +local function TransformMapLinks(text) + if not text or type(text) ~= "string" then return text end + local result = string.gsub(text, "]*)>", + function(c, z, x, y, name) + local display = name + if not display or display == "" then display = "地图标记" end + local _wh = (SFrames.Theme and SFrames.Theme:GetAccentHex()) or "ffFFB3D9" + return "|c" .. _wh .. "|Hnmwp:" .. c .. ":" .. z .. ":" .. x .. ":" .. y .. "|h[" .. display .. " (" .. x .. ", " .. y .. ")]|h|r" + end) + return result +end + +local function HookChatFrameForMapLinks(cf) + if not cf or not cf.AddMessage or cf._nanamiMapLinkHooked then return end + cf._nanamiMapLinkHooked = true + local orig = cf.AddMessage + cf.AddMessage = function(self, text, r, g, b, alpha, holdTime) + if text and type(text) == "string" then + text = TransformMapLinks(text) + end + orig(self, text, r, g, b, alpha, holdTime) + end +end + +local function HookAllChatFramesForMapLinks() + local _G = getfenv(0) + for i = 1, 10 do + local cf = _G["ChatFrame" .. i] + if cf then HookChatFrameForMapLinks(cf) end + end +end + +local function HookSetItemRefForWaypoints() + local _G = getfenv(0) + local origSIR = _G.SetItemRef + _G.SetItemRef = function(link, text, button) + if link and string.sub(link, 1, 5) == "nmwp:" then + local data = string.sub(link, 6) + WM:HandleWaypointLink(data) + return + end + if origSIR then + origSIR(link, text, button) + end + end +end + +-------------------------------------------------------------------------------- +-- 10. WorldMapButton Click Hook for Placing Waypoints +-------------------------------------------------------------------------------- +local function HookWorldMapButtonClick() + if not WorldMapButton then return end + local prevOnClick = WorldMapButton:GetScript("OnClick") + WorldMapButton:SetScript("OnClick", function() + if arg1 == "LeftButton" and IsControlKeyDown() then + local mx, my = GetMapCursorPos() + if mx and my then + local c = GetCurrentMapContinent and GetCurrentMapContinent() or 0 + local z = GetCurrentMapZone and GetCurrentMapZone() or 0 + local name = GetCurrentMapName() + WM:SetWaypoint(c, z, mx, my, name) + end + return + end + if prevOnClick then prevOnClick() end + end) +end + +-------------------------------------------------------------------------------- +-- 11. Navigation Map (迷你全区域地图 - 类似正式服 Shift+M) +-- Shows the entire zone map scaled down. All state in table N. +-------------------------------------------------------------------------------- +local N = { + MW = 1002, MH = 668, + CW = { 256, 256, 256, 234 }, + RH = { 256, 256, 156 }, + DEF_W = 350, MIN_W = 200, MAX_W = 600, + curMap = "", pulse = 0, + overlays = {}, overlayCount = 0, +} +local NC = { + PBG = _A.panelBg, PBD = _A.panelBorder, TC = _A.title, DT = _A.dimText, +} + +local function GetNavConfig() + local cfg = WM:GetConfig() + if type(cfg.nav) ~= "table" then + cfg.nav = { enabled = false, width = N.DEF_W, alpha = 0.70, locked = false } + end + if not cfg.nav.width then cfg.nav.width = N.DEF_W end + if cfg.nav.width < N.MIN_W then cfg.nav.width = N.MIN_W end + if cfg.nav.width > N.MAX_W then cfg.nav.width = N.MAX_W end + return cfg.nav +end + +local function SaveNavPos() + if not N.frame then return end + local nc = GetNavConfig() + local pt, _, rp, x, y = N.frame:GetPoint() + nc.aF = pt; nc.aT = rp; nc.pX = x; nc.pY = y +end + +local function NavNextPow2(n) + local p = 16 + while p < n do p = p * 2 end + return p +end + + +local function NavApplySize(w) + if not N.frame or not N.tiles then return end + local h = w * N.MH / N.MW + local s = w / N.MW + N.frame:SetWidth(w); N.frame:SetHeight(h) + + local idx = 0 + for row = 1, 3 do + for col = 1, 4 do + idx = idx + 1 + local t = N.tiles[idx] + t:SetWidth(N.CW[col] * s); t:SetHeight(N.RH[row] * s) + local xO, yO = 0, 0 + for c = 1, col - 1 do xO = xO + N.CW[c] end + for r = 1, row - 1 do yO = yO + N.RH[r] end + t:ClearAllPoints() + t:SetPoint("TOPLEFT", N.frame, "TOPLEFT", xO * s, -yO * s) + end + end + + for i = 1, N.overlayCount do + if N.overlays[i] and N.ovData and N.ovData[i] then + local d = N.ovData[i] + local tex = N.overlays[i] + tex:SetWidth(d.pw * s); tex:SetHeight(d.ph * s) + tex:ClearAllPoints() + tex:SetPoint("TOPLEFT", N.frame, "TOPLEFT", d.ox * s, -d.oy * s) + end + end +end + +local function UpdateNavOverlays(mapFile) + if not N.frame or not mapFile or mapFile == "" then return end + + local db = MapOverlayData or LibMapOverlayData or zMapOverlayData or mapOverlayData + if not db then + for i = 1, N.overlayCount do if N.overlays[i] then N.overlays[i]:Hide() end end + return + end + + local zoneData = db[mapFile] + if not zoneData then + for i = 1, N.overlayCount do if N.overlays[i] then N.overlays[i]:Hide() end end + return + end + + local nc = GetNavConfig() + local s = (nc.width or N.DEF_W) / N.MW + local prefix = "Interface\\WorldMap\\" .. mapFile .. "\\" + local texIdx = 0 + if not N.ovData then N.ovData = {} end + + for idx = 1, table.getn(zoneData) do + local entry = zoneData[idx] + local _, _, oName, sW, sH, sX, sY = string.find(entry, "^(%u+):(%d+):(%d+):(%d+):(%d+)$") + if not oName then + _, _, oName, sW, sH, sX, sY = string.find(entry, "^([^:]+):(%d+):(%d+):(%d+):(%d+)$") + end + if oName then + local tw = tonumber(sW) + local th = tonumber(sH) + local ox = tonumber(sX) + local oy = tonumber(sY) + local tName = prefix .. oName + local numH = math.ceil(tw / 256) + local numV = math.ceil(th / 256) + + for row = 1, numV do + local pxH, fileH + if row < numV then + pxH = 256; fileH = 256 + else + pxH = math.mod(th, 256) + if pxH == 0 then pxH = 256 end + fileH = NavNextPow2(pxH) + end + for col = 1, numH do + texIdx = texIdx + 1 + local pxW, fileW + if col < numH then + pxW = 256; fileW = 256 + else + pxW = math.mod(tw, 256) + if pxW == 0 then pxW = 256 end + fileW = NavNextPow2(pxW) + end + + if not N.overlays[texIdx] then + N.overlays[texIdx] = N.frame:CreateTexture(nil, "OVERLAY") + end + local tex = N.overlays[texIdx] + local realOx = ox + 256 * (col - 1) + local realOy = oy + 256 * (row - 1) + + N.ovData[texIdx] = { pw = pxW, ph = pxH, ox = realOx, oy = realOy } + + tex:SetWidth(pxW * s); tex:SetHeight(pxH * s) + tex:SetTexCoord(0, pxW / fileW, 0, pxH / fileH) + tex:ClearAllPoints() + tex:SetPoint("TOPLEFT", N.frame, "TOPLEFT", realOx * s, -realOy * s) + + local tileIndex = ((row - 1) * numH) + col + tex:SetTexture(tName .. tileIndex) + tex:SetVertexColor(1, 1, 1, 1) + tex:Show() + end + end + end + end + + for i = texIdx + 1, N.overlayCount do + if N.overlays[i] then N.overlays[i]:Hide() end + end + if texIdx > N.overlayCount then N.overlayCount = texIdx end +end + +local function CreateNavMap() + if N.frame then return end + local nc = GetNavConfig() + local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + local w = nc.width or N.DEF_W + local h = w * N.MH / N.MW + local s = w / N.MW + + local f = CreateFrame("Frame", "NanamiNavMap", UIParent) + N.frame = f + f:SetWidth(w); f:SetHeight(h) + f:SetAlpha(nc.alpha or 0.70) + f:SetMovable(true); f:EnableMouse(true); f:EnableMouseWheel(1) + f:SetClampedToScreen(true) + f:SetFrameStrata("LOW"); f:SetFrameLevel(10) + f:Hide() + + f:ClearAllPoints() + if nc.aF and nc.pX and nc.pY then + f:SetPoint(nc.aF, UIParent, nc.aT or nc.aF, nc.pX, nc.pY) + else + f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + end + + N.tiles = {} + local idx = 0 + for row = 1, 3 do + for col = 1, 4 do + idx = idx + 1 + local oW = N.CW[col] + local oH = N.RH[row] + local tile = f:CreateTexture("NanamiNavTile" .. idx, "ARTWORK") + tile:SetWidth(oW * s); tile:SetHeight(oH * s) + local xO, yO = 0, 0 + for c = 1, col - 1 do xO = xO + N.CW[c] end + for r = 1, row - 1 do yO = yO + N.RH[r] end + tile:SetPoint("TOPLEFT", f, "TOPLEFT", xO * s, -yO * s) + local tcR = (oW >= 256) and 1 or (oW / 256) + local tcB = (oH >= 256) and 1 or (oH / 256) + tile:SetTexCoord(0, tcR, 0, tcB) + N.tiles[idx] = tile + end + end + + -- thin border + local borderFrame = CreateFrame("Frame", nil, f) + borderFrame:SetAllPoints(f) + borderFrame:SetFrameLevel(f:GetFrameLevel() + 5) + borderFrame:SetBackdrop({ + edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + borderFrame:SetBackdropBorderColor(0, 0, 0, 0.50) + + -- gradient fade overlay (soft vignette at edges) + local fade = CreateFrame("Frame", nil, f) + fade:SetAllPoints(f) + fade:SetFrameLevel(f:GetFrameLevel() + 6) + local FD = 30 + + local fadeL = fade:CreateTexture(nil, "OVERLAY") + fadeL:SetTexture("Interface\\Buttons\\WHITE8X8") + fadeL:SetWidth(FD) + fadeL:SetPoint("TOPLEFT", fade, "TOPLEFT", 0, 0) + fadeL:SetPoint("BOTTOMLEFT", fade, "BOTTOMLEFT", 0, 0) + fadeL:SetGradientAlpha("HORIZONTAL", 0,0,0,0.25, 0,0,0,0) + + local fadeR = fade:CreateTexture(nil, "OVERLAY") + fadeR:SetTexture("Interface\\Buttons\\WHITE8X8") + fadeR:SetWidth(FD) + fadeR:SetPoint("TOPRIGHT", fade, "TOPRIGHT", 0, 0) + fadeR:SetPoint("BOTTOMRIGHT", fade, "BOTTOMRIGHT", 0, 0) + fadeR:SetGradientAlpha("HORIZONTAL", 0,0,0,0, 0,0,0,0.25) + + local fadeT = fade:CreateTexture(nil, "OVERLAY") + fadeT:SetTexture("Interface\\Buttons\\WHITE8X8") + fadeT:SetHeight(FD) + fadeT:SetPoint("TOPLEFT", fade, "TOPLEFT", 0, 0) + fadeT:SetPoint("TOPRIGHT", fade, "TOPRIGHT", 0, 0) + fadeT:SetGradientAlpha("VERTICAL", 0,0,0,0, 0,0,0,0.25) + + local fadeB = fade:CreateTexture(nil, "OVERLAY") + fadeB:SetTexture("Interface\\Buttons\\WHITE8X8") + fadeB:SetHeight(FD) + fadeB:SetPoint("BOTTOMLEFT", fade, "BOTTOMLEFT", 0, 0) + fadeB:SetPoint("BOTTOMRIGHT", fade, "BOTTOMRIGHT", 0, 0) + fadeB:SetGradientAlpha("VERTICAL", 0,0,0,0.25, 0,0,0,0) + + -- player indicator (arrow + ripple) + local dotFrame = CreateFrame("Frame", nil, f) + N.dotFrame = dotFrame + dotFrame:SetWidth(1); dotFrame:SetHeight(1) + dotFrame:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) + dotFrame:SetFrameLevel(f:GetFrameLevel() + 7) + + local CIRCLE = "Interface\\Minimap\\UI-Minimap-Background" + N.ripples = {} + for i = 1, 3 do + local rip = dotFrame:CreateTexture(nil, "ARTWORK") + rip:SetTexture(CIRCLE) + rip:SetWidth(8); rip:SetHeight(8) + rip:SetPoint("CENTER", dotFrame, "CENTER", 0, 0) + rip:SetVertexColor(0.0, 0.85, 1.0, 0.35) + rip:SetBlendMode("ADD") + N.ripples[i] = rip + end + + local CLASS_ICON = "Interface\\AddOns\\Nanami-UI\\img\\UI-Classes-Circles" + N.dotTex = dotFrame:CreateTexture(nil, "OVERLAY") + N.dotTex:SetTexture(CLASS_ICON) + N.dotTex:SetWidth(14); N.dotTex:SetHeight(14) + N.dotTex:SetPoint("CENTER", dotFrame, "CENTER", 0, 0) + local _, pClass = UnitClass("player") + if pClass and SFrames.CLASS_ICON_TCOORDS and SFrames.CLASS_ICON_TCOORDS[pClass] then + local tc = SFrames.CLASS_ICON_TCOORDS[pClass] + N.dotTex:SetTexCoord(tc[1], tc[2], tc[3], tc[4]) + end + + N.dirTip = dotFrame:CreateTexture(nil, "OVERLAY") + N.dirTip:SetTexture(CIRCLE) + N.dirTip:SetWidth(4); N.dirTip:SetHeight(4) + N.dirTip:SetPoint("CENTER", dotFrame, "CENTER", 0, 10) + N.dirTip:SetVertexColor(1, 1, 1, 0.90) + + N.dirTail = dotFrame:CreateTexture(nil, "OVERLAY") + N.dirTail:SetTexture(CIRCLE) + N.dirTail:SetWidth(2); N.dirTail:SetHeight(2) + N.dirTail:SetPoint("CENTER", dotFrame, "CENTER", 0, -7) + N.dirTail:SetVertexColor(0.6, 0.6, 0.6, 0.50) + + -- info bar (zone + coords below map) + local infoFrame = CreateFrame("Frame", nil, f) + infoFrame:SetWidth(w); infoFrame:SetHeight(24) + infoFrame:SetPoint("TOP", f, "BOTTOM", 0, 0) + infoFrame:SetFrameLevel(f:GetFrameLevel() + 6) + + N.zoneFS = infoFrame:CreateFontString(nil, "OVERLAY") + N.zoneFS:SetFont(font, 9, "OUTLINE") + N.zoneFS:SetPoint("LEFT", infoFrame, "LEFT", 4, 0) + N.zoneFS:SetTextColor(NC.TC[1], NC.TC[2], NC.TC[3]) + + N.coordFS = infoFrame:CreateFontString(nil, "OVERLAY") + N.coordFS:SetFont(font, 9, "OUTLINE") + N.coordFS:SetPoint("RIGHT", infoFrame, "RIGHT", -4, 0) + N.coordFS:SetTextColor(0.85, 0.85, 0.85) + + -- close button + local cbtn = CreateFrame("Button", nil, f) + cbtn:SetWidth(14); cbtn:SetHeight(14) + cbtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -3, -3) + cbtn:SetFrameLevel(f:GetFrameLevel() + 8) + local cbFS = cbtn:CreateFontString(nil, "OVERLAY") + cbFS:SetFont(font, 11, "OUTLINE"); cbFS:SetPoint("CENTER", 0, 0) + cbFS:SetText("x"); cbFS:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3]) + cbtn:SetScript("OnClick", function() WM:ToggleNav() end) + cbtn:SetScript("OnEnter", function() cbFS:SetTextColor(1, 0.3, 0.3) end) + cbtn:SetScript("OnLeave", function() cbFS:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3]) end) + + -- hover toolbar (shown on mouse enter) + local toolbar = CreateFrame("Frame", nil, f) + toolbar:SetWidth(w); toolbar:SetHeight(22) + toolbar:SetPoint("BOTTOM", f, "TOP", 0, 2) + toolbar:SetFrameLevel(f:GetFrameLevel() + 9) + toolbar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", + edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) + toolbar:SetBackdropColor(_A.headerBg[1], _A.headerBg[2], _A.headerBg[3], 0.85) + toolbar:SetBackdropBorderColor(0, 0, 0, 0.50) + toolbar:EnableMouse(true) + toolbar:Hide() + N.toolbar = toolbar + + local tipFS = toolbar:CreateFontString(nil, "OVERLAY") + tipFS:SetFont(font, 9, "OUTLINE") + tipFS:SetPoint("CENTER", toolbar, "CENTER", 0, 0) + tipFS:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3]) + tipFS:SetText("拖动移位 | 滚轮缩放 | Ctrl+滚轮透明度") + + local lockBtn = CreateFrame("Button", nil, toolbar) + lockBtn:SetWidth(28); lockBtn:SetHeight(16) + lockBtn:SetPoint("RIGHT", toolbar, "RIGHT", -4, 0) + lockBtn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", + edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) + lockBtn:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], 0.90) + lockBtn:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.6) + local lockFS = lockBtn:CreateFontString(nil, "OVERLAY") + lockFS:SetFont(font, 8, "OUTLINE"); lockFS:SetPoint("CENTER", 0, 0) + N.lockFS = lockFS + + local function UpdateLockText() + local lk = GetNavConfig().locked + if lk then + lockFS:SetText("解锁"); lockFS:SetTextColor(1.0, 0.5, 0.5) + else + lockFS:SetText("锁定"); lockFS:SetTextColor(0.5, 1.0, 0.5) + end + end + UpdateLockText() + + lockBtn:SetScript("OnClick", function() + local cfg = GetNavConfig() + cfg.locked = not cfg.locked + UpdateLockText() + end) + lockBtn:SetScript("OnEnter", function() + this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4]) + end) + lockBtn:SetScript("OnLeave", function() + this:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.6) + end) + + N.hideTimer = 0 + f:SetScript("OnEnter", function() + N.hideTimer = 0 + if N.toolbar then N.toolbar:Show() end + if cbtn then cbtn:Show() end + end) + f:SetScript("OnLeave", function() + N.hideTimer = 1.5 + end) + toolbar:SetScript("OnEnter", function() + N.hideTimer = 0 + end) + toolbar:SetScript("OnLeave", function() + N.hideTimer = 1.5 + end) + cbtn:Hide() + + -- dragging + f:SetScript("OnMouseDown", function() + if arg1 == "LeftButton" and not GetNavConfig().locked then this:StartMoving() end + end) + f:SetScript("OnMouseUp", function() this:StopMovingOrSizing(); SaveNavPos() end) + + -- scroll: resize from center / alpha + f:SetScript("OnMouseWheel", function() + local cfg = GetNavConfig() + if IsControlKeyDown() then + local a = (cfg.alpha or this:GetAlpha()) + arg1 * 0.10 + if a < 0.15 then a = 0.15 end; if a > 1 then a = 1 end + cfg.alpha = a; this:SetAlpha(a) + else + local oldW = this:GetWidth() + local oldH = this:GetHeight() + local nw = cfg.width + arg1 * 25 + if nw < N.MIN_W then nw = N.MIN_W end + if nw > N.MAX_W then nw = N.MAX_W end + local nh = nw * N.MH / N.MW + local dw = (nw - oldW) / 2 + local dh = (nh - oldH) / 2 + + local pt, rel, rp, px, py = this:GetPoint() + if pt and px and py then + this:ClearAllPoints() + this:SetPoint(pt, rel, rp, px - dw, py + dh) + end + + cfg.width = nw + NavApplySize(nw) + if N.toolbar then N.toolbar:SetWidth(nw) end + SaveNavPos() + end + end) +end + +local function UpdateNavMap() + if not N.frame or not N.tiles or not N.frame:IsVisible() then return end + if WorldMapFrame and WorldMapFrame:IsVisible() then return end + + if SetMapToCurrentZone then SetMapToCurrentZone() end + + local mapFile = GetMapInfo and GetMapInfo() or "" + if mapFile ~= "" and mapFile ~= N.curMap then + N.curMap = mapFile + for i = 1, 12 do + N.tiles[i]:SetTexture("Interface\\WorldMap\\" .. mapFile .. "\\" .. mapFile .. i) + end + UpdateNavOverlays(mapFile) + end + + if mapFile ~= "" then + local px, py = GetPlayerMapPosition("player") + if px and py and (px > 0 or py > 0) then + local fw = N.frame:GetWidth() + local fh = N.frame:GetHeight() + if N.dotFrame then + N.dotFrame:ClearAllPoints() + N.dotFrame:SetPoint("TOPLEFT", N.frame, "TOPLEFT", px * fw, -py * fh) + end + + if GetPlayerFacing and N.dirTip then + local facing = GetPlayerFacing() + if facing then + local r = 10 + local tipX = -math.sin(facing) * r + local tipY = math.cos(facing) * r + N.dirTip:ClearAllPoints() + N.dirTip:SetPoint("CENTER", N.dotFrame, "CENTER", tipX, tipY) + if N.dirTail then + local tr = 7 + N.dirTail:ClearAllPoints() + N.dirTail:SetPoint("CENTER", N.dotFrame, "CENTER", -tipX / r * tr, -tipY / r * tr) + end + end + end + + local coordStr = string.format("%.1f, %.1f", px * 100, py * 100) + if N.coordFS then N.coordFS:SetText(coordStr) end + end + end + + local zn = GetZoneText and GetZoneText() or "" + if zn ~= "" and N.zoneFS then N.zoneFS:SetText(zn) end +end + +local function UpdateNavPulse(dt) + if not N.frame or not N.ripples or not N.frame:IsVisible() then return end + N.pulse = (N.pulse or 0) + 0.05 + if N.pulse > 6.2832 then N.pulse = N.pulse - 6.2832 end + + local PHASE_OFF = 2.0944 + for i = 1, 3 do + local rip = N.ripples[i] + if rip then + local t = math.mod(N.pulse + (i - 1) * PHASE_OFF, 6.2832) / 6.2832 + local sz = 6 + 28 * t + local a = 0.40 * (1 - t) + rip:SetWidth(sz); rip:SetHeight(sz) + rip:SetVertexColor(0.0, 0.85, 1.0, a) + end + end + + if N.hideTimer and N.hideTimer > 0 then + N.hideTimer = N.hideTimer - (dt or 0.04) + if N.hideTimer <= 0 then + N.hideTimer = 0 + if N.toolbar then N.toolbar:Hide() end + end + end +end + +function WM:ToggleNav() + local nc = GetNavConfig() + nc.enabled = not nc.enabled + CreateNavMap() + if nc.enabled then + N.frame:Show(); N.curMap = "" + UpdateNavMap() + if SFrames and SFrames.Print then + SFrames:Print("导航地图: |cff00ff00已开启|r (滚轮缩放 | Ctrl+滚轮透明度 | 拖动)") + end + else + N.frame:Hide() + if SFrames and SFrames.Print then + SFrames:Print("导航地图: |cffff0000已关闭|r") + end + end +end + +function WM:ShowNav() + local nc = GetNavConfig() + CreateNavMap() + if not nc.enabled then nc.enabled = true end + if WorldMapFrame and WorldMapFrame:IsVisible() then + WorldMapFrame:Hide() + end + N.frame:Show(); N.curMap = "" + UpdateNavMap() +end + +local function CreateNavToggleBtn() + if not skinFrame or not coordOverlay then return end + if N.toggleBtn then return end + local font = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" + local b = CreateFrame("Button", nil, coordOverlay) + N.toggleBtn = b + b:SetWidth(70); b:SetHeight(20) + b:SetPoint("BOTTOMRIGHT", skinFrame, "BOTTOMRIGHT", -12, 22) + b:SetFrameLevel(coordOverlay:GetFrameLevel() + 1) + b:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", + edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) + b:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4]) + b:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4]) + local fs = b:CreateFontString(nil, "OVERLAY") + fs:SetFont(font, 10, "OUTLINE"); fs:SetPoint("CENTER", 0, 0) + fs:SetText("缩略地图"); fs:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3]) + b:SetScript("OnClick", function() WM:ShowNav() end) + b:SetScript("OnEnter", function() + this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4]) + this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4]) + end) + b:SetScript("OnLeave", function() + this:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4]) + this:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4]) + end) +end + +local function InitNavMap() + CreateNavMap() + CreateNavToggleBtn() + + local nc = GetNavConfig() + if nc.enabled then + N.frame:Show(); N.curMap = "" + end + + local mapEl, animEl = 0, 0 + local u = CreateFrame("Frame", nil, UIParent) + u:SetScript("OnUpdate", function() + local dt = arg1 or 0 + mapEl = mapEl + dt + animEl = animEl + dt + if mapEl >= 0.10 then mapEl = 0; UpdateNavMap() end + if animEl >= 0.04 then + local elapsed = animEl; animEl = 0; UpdateNavPulse(elapsed) + end + end) +end + +-------------------------------------------------------------------------------- +-- 12. Initialize +-------------------------------------------------------------------------------- +function WM:Initialize() + local cfg = self:GetConfig() + if not cfg.enabled then return end + + HideBlizzardDecorations() + CreateNanamiSkin() + CreateTooltipBG() + self:SetupWindowMode() + + SkinWorldMapControls() + CreateWaypointPin() + HookWorldMapButtonClick() + + HookTooltipShow(WorldMapTooltip) + HookTooltipShow(GameTooltip) + + HookAllChatFramesForMapLinks() + HookSetItemRefForWaypoints() + + local origUpdater = OnFrameUpdate + local function NewOnFrameUpdate() + origUpdater() + UpdatePinPosition() + UpdateDarkmoonPins() + end + + local updater = CreateFrame("Frame", nil, UIParent) + updater:SetScript("OnUpdate", NewOnFrameUpdate) + + InitNavMap() + DiscoverDmfZoneIndices() + + self.initialized = true + local _hex = (SFrames.Theme and SFrames.Theme:GetAccentHex()) or "ffffb3d9" + DEFAULT_CHAT_FRAME:AddMessage("|c" .. _hex .. "[Nanami-UI]|r 世界地图模块已加载 (Nanami主题 | Ctrl+左键放置标记 | /nui nav 导航地图)") +end diff --git a/agent-tools/MAGE_WARLOCK_DRUID_skills.lua b/agent-tools/MAGE_WARLOCK_DRUID_skills.lua new file mode 100644 index 0000000..2942707 --- /dev/null +++ b/agent-tools/MAGE_WARLOCK_DRUID_skills.lua @@ -0,0 +1,96 @@ +MAGE = { + [4] = {"造水术", "寒冰箭"}, + [6] = {"造食术", "火球术 2级", "火焰冲击", "魔法抑制", "造水术 2级"}, + [8] = {"变形术", "奥术飞弹"}, + [10] = {"霜甲术 2级", "冰霜新星"}, + [12] = {"缓落术", "造食术 2级", "魔法抑制", "火球术 3级"}, + [14] = {"魔爆术", "奥术智慧 2级", "奥术飞弹 3级", "火焰冲击 2级"}, + [16] = {"奥术飞弹 2级", "侦测魔法", "烈焰风暴"}, + [18] = {"解除次级诅咒", "魔法增效", "造水术 4级", "火球术 4级"}, + [20] = {"变形术 2级", "造水术 3级", "法力护盾", "闪现术", "传送:暴风城", "传送:铁炉堡", "传送:幽暗城", "传送:奥格瑞玛", "防护火焰结界", "霜甲术 3级", "寒冰箭 4级", "暴风雪", "唤醒"}, + [22] = {"造食术 3级", "魔爆术 2级", "火焰冲击 3级", "灼烧"}, + [24] = {"魔法抑制 2级", "火球术 5级", "奥术飞弹 4级", "烈焰风暴 2级", "法术反制", "防护冰霜结界"}, + [26] = {"寒冰箭 5级", "冰锥术"}, + [28] = {"制造魔法玛瑙", "奥术智慧 3级", "法力护盾 2级", "暴风雪 2级", "灼烧 2级", "冰霜新星 2级"}, + [30] = {"魔爆术 3级", "火球术 6级", "传送:达纳苏斯", "传送:雷霆崖", "防护火焰结界 2级", "冰甲术"}, + [32] = {"造食术 4级", "奥术飞弹 4级", "烈焰风暴 3级", "寒冰箭 6级", "防护冰霜结界 2级"}, + [34] = {"魔甲术", "冰锥术 2级", "灼烧 3级"}, + [36] = {"魔法抑制 3级", "法力护盾 3级", "火球术 7级", "暴风雪 3级", "冰霜新星 3级"}, + [38] = {"魔爆术 4级", "制造魔法翡翠", "寒冰箭 7级", "火焰冲击 5级"}, + [40] = {"造食术 5级", "奥术飞弹 5级", "传送门:暴风城", "传送门:铁炉堡", "传送门:奥格瑞玛", "传送门:幽暗城", "火球术 8级", "冰甲术 2级", "灼烧 4级"}, + [42] = {"魔法增效 3级", "奥术智慧 4级", "火球术 8级", "防护冰霜结界 3级"}, + [44] = {"法力护盾 4级", "暴风雪 4级", "寒冰箭 8级"}, + [46] = {"魔爆术 5级", "灼烧 5级"}, + [48] = {"魔法抑制 4级", "制造魔法黄水晶", "火球术 9级", "奥术飞弹 6级", "烈焰风暴 5级"}, + [50] = {"造水术 6级", "寒冰箭 9级", "冰锥术 4级", "防护火焰结界 4级", "传送门:达纳苏斯", "传送门:雷霆崖", "冰甲术 3级"}, + [52] = {"法力护盾 5级", "火球术 10级", "火焰冲击 7级", "防护冰霜结界 4级", "冰霜新星 4级"}, + [54] = {"魔法增效 4级", "奥术飞弹 7级", "烈焰风暴 6级"}, + [56] = {"奥术智慧 5级", "寒冰箭 10级", "冰锥术 5级"}, + [58] = {"魔甲术 3级", "制造魔法红宝石", "灼烧 7级"}, + [60] = {"变形术 4级", "魔法抑制 5级", "法力护盾 6级", "火球术 11级", "防护火焰结界 5级", "暴风雪 6级", "冰甲术 4级"}, +}, + +WARLOCK = { + [2] = {"痛苦诅咒", "恐惧术"}, + [4] = {"腐蚀术", "虚弱诅咒"}, + [6] = {"虚弱诅咒 2级", "暗影箭 3级"}, + [8] = {"痛苦诅咒"}, + [10] = {"吸取灵魂", "献祭 2级", "恶魔皮肤 2级", "制造初级治疗石"}, + [12] = {"生命分流 2级", "生命通道", "魔息术"}, + [14] = {"腐蚀术 2级", "吸取生命", "鲁莽诅咒"}, + [16] = {"生命分流 2级"}, + [18] = {"痛苦诅咒 2级", "制造初级灵魂石", "灼热之痛"}, + [20] = {"献祭 3级", "生命通道 2级", "暗影箭 4级", "召唤仪式", "魔甲术", "火焰之雨"}, + [22] = {"吸取生命 2级", "虚弱诅咒 3级", "基尔罗格之眼", "制造次级治疗石"}, + [24] = {"腐蚀术 3级", "吸取灵魂 2级", "吸取法力", "感知恶魔"}, + [26] = {"生命分流 3级", "语言诅咒", "侦测次级隐形"}, + [28] = {"鲁莽诅咒 2级", "痛苦诅咒 3级", "生命通道 3级", "放逐术", "制造次级火焰石"}, + [30] = {"吸取生命 3级", "献祭 4级", "奴役恶魔", "制造次级灵魂石", "地狱烈焰", "魔甲术 2级"}, + [32] = {"虚弱诅咒 4级", "恐惧术 2级", "元素诅咒", "防护暗影结界"}, + [34] = {"生命分流 4级", "吸取法力 2级", "火焰之雨 2级", "制造治疗石", "灼热之痛 3级"}, + [36] = {"生命通道 4级", "制造法术石", "制造火焰石"}, + [38] = {"吸取灵魂 3级", "痛苦诅咒 4级", "生命虹吸 2级", "侦测隐形"}, + [40] = {"恐惧嚎叫", "献祭 5级", "制造灵魂石", "奴役恶魔 2级"}, + [42] = {"虚弱诅咒 5级", "鲁莽诅咒 3级", "死亡缠绕", "防护暗影结界 2级", "地狱烈焰 2级", "灼热之痛 4级"}, + [44] = {"吸取生命 5级", "生命通道 5级", "暗影诅咒", "暗影箭 7级"}, + [46] = {"生命分流 5级", "制造强效治疗石", "制造强效火焰石", "火焰之雨 3级"}, + [48] = {"痛苦诅咒 5级", "放逐术 2级", "灵魂之火", "制造强效法术石"}, + [50] = {"虚弱诅咒 6级", "死亡缠绕 2级", "恐惧嚎叫 2级", "魔甲术 4级", "吸取灵魂 4级", "吸取法力 4级", "生命虹吸 3级", "黑暗契约 2级", "侦测强效隐形", "暗影箭 8级", "灼热之痛 5级"}, + [52] = {"防护暗影结界 3级", "生命通道 6级"}, + [54] = {"腐蚀术 6级", "吸取生命 6级", "地狱烈焰 3级", "灵魂之火 2级"}, + [56] = {"鲁莽诅咒 4级", "暗影诅咒 2级", "死亡缠绕 3级", "生命虹吸 4级", "制造特效火焰石"}, + [58] = {"痛苦诅咒 6级", "奴役恶魔 3级", "火焰之雨 4级", "制造特效治疗石", "灼热之痛 6级"}, + [60] = {"厄运诅咒", "元素诅咒 3级", "魔甲术 5级", "制造特效法术石", "暗影箭 9级"}, +}, + +DRUID = { + [4] = {"月火术", "回春术"}, + [6] = {"荆棘术", "愤怒 2级"}, + [8] = {"纠缠根须", "治疗之触 2级"}, + [10] = {"月火术 2级", "回春术 2级", "挫志咆哮", "野性印记 2级"}, + [12] = {"愈合", "狂怒"}, + [14] = {"荆棘术 2级", "愤怒 3级", "重击"}, + [16] = {"月火术 3级", "回春术 3级", "挥击"}, + [18] = {"精灵之火", "休眠", "愈合 2级", "槌击 2级"}, + [20] = {"纠缠根须 2级", "星火术", "月火术 4级", "回春术 4级", "挫志咆哮 2级", "猎豹形态", "撕扯", "爪击", "治疗之触 4级", "潜行", "野性印记 3级", "复生"}, + [22] = {"月火术 4级", "回春术 4级", "愤怒 4级", "撕碎", "安抚动物"}, + [24] = {"荆棘术 3级", "挥击 2级", "扫击", "猛虎之怒", "撕碎", "解除诅咒"}, + [26] = {"星火术 2级", "月火术 5级", "槌击 3级", "爪击 2级", "治疗之触 5级", "驱毒术"}, + [28] = {"撕扯 2级", "挑战咆哮", "畏缩"}, + [30] = {"精灵之火 2级", "星火术 3级", "愤怒 5级", "旅行形态", "撕碎 2级", "重击 2级", "野性印记 4级", "宁静", "复生 2级", "虫群 2级"}, + [32] = {"挫志咆哮 3级", "挥击 3级", "毁灭", "撕碎 3级", "治疗之触 6级", "追踪人型生物", "凶猛撕咬"}, + [34] = {"荆棘术 4级", "月火术 6级", "回春术 6级", "槌击 4级", "扫击 2级", "爪击 3级"}, + [36] = {"愤怒 6级", "突袭", "狂暴回复"}, + [38] = {"纠缠根须 4级", "休眠 2级", "安抚动物 2级", "爪击 3级", "撕碎 3级"}, + [40] = {"星火术 4级", "飓风", "挥击 4级", "潜行 2级", "畏缩 2级", "巨熊形态", "豹之优雅", "凶猛撕咬 2级", "回春术 7级", "宁静 2级", "复生 3级", "虫群 3级", "激活"}, + [42] = {"挫志咆哮 4级", "槌击 5级", "毁灭 2级"}, + [44] = {"荆棘术 5级", "树皮术", "撕扯 4级", "扫击 3级", "治疗之触 8级"}, + [46] = {"愤怒 7级", "重击 3级", "突袭 2级"}, + [48] = {"纠缠根须 5级", "月火术 8级", "猛虎之怒 3级", "撕碎 4级"}, + [50] = {"星火术 5级", "槌击 6级", "宁静 3级", "复生 4级", "虫群 4级"}, + [52] = {"挫志咆哮 5级", "撕扯 5级", "畏缩 3级", "凶猛撕咬 4级", "回春术 9级"}, + [54] = {"荆棘术 6级", "愤怒 8级", "月火术 9级", "挥击 5级", "扫击 4级", "爪击 4级"}, + [56] = {"凶猛撕咬 4级", "治疗之触 10级"}, + [58] = {"纠缠根须 6级", "星火术 6级", "月火术 10级", "爪击 5级", "槌击 7级", "毁灭 4级", "回春术 10级"}, + [60] = {"飓风 3级", "潜行 3级", "猛虎之怒 4级", "撕扯 6级", "宁静 4级", "复生 5级", "虫群 5级", "野性印记 7级", "愈合 9级"}, +}, diff --git a/agent-tools/TALENT_TRAINER_SKILLS.lua b/agent-tools/TALENT_TRAINER_SKILLS.lua new file mode 100644 index 0000000..7d90e7e --- /dev/null +++ b/agent-tools/TALENT_TRAINER_SKILLS.lua @@ -0,0 +1,73 @@ +--[[ +TALENT-based trainer skill data for WoW Classic +Skills where Rank 1 comes from talent points, higher ranks from class trainer. +Only includes ranks that are NOT "默认开启" and have EVEN required levels. +]] + +local TALENT_TRAINER_SKILLS = { + -- WARRIOR: 致死打击, 嗜血, 盾牌猛击 - NOT FOUND in provided warrior file + WARRIOR = { + -- 致死打击 (Mortal Strike), 嗜血 (Bloodthirst), 盾牌猛击 (Shield Slam) + -- These skills were not found in the warrior class data file. + }, + + -- ROGUE: 出血 (Hemorrhage) - Rank 1 from talent (默认开启), Ranks 2-3 from trainer + ROGUE = { + {base="出血", level=46, display="出血 2级"}, + {base="出血", level=58, display="出血 3级"}, + }, + + -- PRIEST: 精神鞭笞 (Mind Flay) - Rank 1 from talent (默认开启), Ranks 2-6 from trainer + PRIEST = { + {base="精神鞭笞", level=28, display="精神鞭笞 2级"}, + {base="精神鞭笞", level=36, display="精神鞭笞 3级"}, + {base="精神鞭笞", level=44, display="精神鞭笞 4级"}, + {base="精神鞭笞", level=52, display="精神鞭笞 5级"}, + {base="精神鞭笞", level=60, display="精神鞭笞 6级"}, + }, + + -- HUNTER: 瞄准射击, 反击, 翼龙钉刺 - NOT FOUND in provided hunter file + HUNTER = { + -- 瞄准射击 (Aimed Shot), 反击 (Counterattack), 翼龙钉刺 (Wyvern Sting) + -- These skills were not found in the hunter class data file. + }, + + -- MAGE: 炎爆术 (Pyroblast) - Rank 1 from talent (默认开启), Ranks 2-8 from trainer + -- 冲击波 (Blast Wave), 寒冰屏障 (Ice Barrier) - NOT FOUND in provided mage file + MAGE = { + {base="炎爆术", level=24, display="炎爆术 2级"}, + {base="炎爆术", level=30, display="炎爆术 3级"}, + {base="炎爆术", level=36, display="炎爆术 4级"}, + {base="炎爆术", level=42, display="炎爆术 5级"}, + {base="炎爆术", level=48, display="炎爆术 6级"}, + {base="炎爆术", level=54, display="炎爆术 7级"}, + {base="炎爆术", level=60, display="炎爆术 8级"}, + -- 冲击波 (Blast Wave), 寒冰屏障 (Ice Barrier) - not found in file + }, + + -- PALADIN: 神圣震击 (Holy Shock) - NOT FOUND in provided paladin file + PALADIN = { + -- 神圣震击 (Holy Shock) was not found in the paladin class data file. + }, + + -- WARLOCK: 暗影灼烧 (Shadowburn) - NOT FOUND; 生命虹吸, 黑暗契约 found; 灵魂之火 skipped per user + WARLOCK = { + {base="生命虹吸", level=38, display="生命虹吸 2级"}, + {base="生命虹吸", level=48, display="生命虹吸 3级"}, + {base="生命虹吸", level=58, display="生命虹吸 4级"}, + {base="黑暗契约", level=50, display="黑暗契约 2级"}, + {base="黑暗契约", level=60, display="黑暗契约 3级"}, + -- 暗影灼烧 (Shadowburn) - not found in file + -- 灵魂之火 - skipped (already in regular data per user) + }, + + -- DRUID: 虫群 (Insect Swarm) - Rank 1 from talent (默认开启), Ranks 2-5 from trainer + DRUID = { + {base="虫群", level=30, display="虫群 2级"}, + {base="虫群", level=40, display="虫群 3级"}, + {base="虫群", level=50, display="虫群 4级"}, + {base="虫群", level=60, display="虫群 5级"}, + }, +} + +return TALENT_TRAINER_SKILLS diff --git a/agent-tools/class_trainer_skills.lua b/agent-tools/class_trainer_skills.lua new file mode 100644 index 0000000..17ee722 --- /dev/null +++ b/agent-tools/class_trainer_skills.lua @@ -0,0 +1,63 @@ +PRIEST = { + [4] = {"暗言术:痛", "次级治疗术 2级"}, + [6] = {"真言术:盾", "惩击 2级"}, + [8] = {"恢复", "渐隐术"}, + [10] = {"暗言术:痛 2级", "心灵震爆", "复活术"}, + [12] = {"真言术:盾 2级", "心灵之火", "真言术:韧 2级", "惩击 3级", "祛病术"}, + [14] = {"恢复 2级", "心灵尖啸", "惩击 3级"}, + [16] = {"治疗术", "心灵震爆 2级"}, + [18] = {"真言术:盾 3级", "驱散魔法", "星辰碎片 2级", "绝望祷言 2级", "暗言术:痛 3级"}, + [20] = {"心灵之火 2级", "束缚亡灵", "回馈 2级", "恢复 3级", "快速治疗", "安抚心灵", "渐隐术 2级", "神圣之火", "虚弱之触 2级", "虚弱妖术 2级"}, + [22] = {"惩击 4级", "心灵视界", "复活术 2级", "心灵震爆 3级"}, + [24] = {"真言术:盾 4级", "真言术:韧 3级", "法力燃烧", "神圣之火 2级"}, + [26] = {"星辰碎片 3级", "恢复 4级", "暗言术:痛 4级", "绝望祷言 3级"}, + [28] = {"治疗术 3级", "心灵震爆 4级", "精神鞭笞 2级", "心灵尖啸 2级", "暗影守卫 2级"}, + [30] = {"真言术:盾 5级", "心灵之火 3级", "艾露恩的赐福 3级", "回馈 2级", "治疗祷言", "束缚亡灵 2级", "虚弱之触 3级", "虚弱妖术 3级", "精神控制", "防护暗影", "渐隐术 3级"}, + [32] = {"法力燃烧 2级", "恢复 5级", "驱除疾病", "快速治疗 3级"}, + [34] = {"漂浮术", "星辰碎片 4级", "暗言术:痛 5级", "心灵震爆 5级", "复活术 3级", "治疗术 4级"}, + [36] = {"真言术:盾 6级", "驱散魔法 2级", "真言术:韧 4级", "噬灵瘟疫 3级", "星辰碎片 5级", "心灵之火 4级", "恢复 6级", "惩击 6级", "精神鞭笞 3级", "暗影守卫 3级", "安抚心灵 2级"}, + [38] = {"恢复 6级", "惩击 6级"}, + [40] = {"心灵之火 4级", "艾露恩的赐福 4级", "法力燃烧 3级", "回馈 3级", "神圣之灵 2级", "治疗祷言 2级", "束缚亡灵 2级", "虚弱之触 4级", "虚弱妖术 4级", "防护暗影 2级", "心灵震爆 6级", "渐隐术 4级"}, + [42] = {"真言术:盾 7级", "星辰碎片 5级", "神圣之火 5级", "心灵尖啸 3级"}, + [44] = {"恢复 7级", "精神控制 2级", "心灵视界 2级"}, + [46] = {"惩击 7级", "强效治疗术 2级", "心灵震爆 7级", "复活术 4级"}, + [48] = {"真言术:盾 8级", "真言术:韧 5级", "法力燃烧 4级", "噬灵瘟疫 4级", "星辰碎片 6级", "神圣之火 6级", "恢复 8级", "暗言术:痛 7级"}, + [50] = {"心灵之火 5级", "艾露恩的赐福 5级", "回馈 4级", "神圣之灵 3级", "治疗祷言 3级", "虚弱之触 5级", "虚弱妖术 5级", "恢复 8级", "绝望祷言 6级"}, + [52] = {"强效治疗术 3级", "心灵震爆 8级", "安抚心灵 3级"}, + [54] = {"真言术:盾 9级", "神圣之火 7级", "惩击 8级"}, + [56] = {"法力燃烧 5级", "恢复 9级", "防护暗影 3级", "心灵尖啸 4级", "暗言术:痛 8级"}, + [58] = {"复活术 5级", "强效治疗术 4级", "心灵震爆 9级"}, + [60] = {"真言术:盾 10级", "心灵之火 6级", "真言术:韧 6级", "束缚亡灵 3级", "艾露恩的赐福 5级", "回馈 5级", "神圣之灵 4级", "精神祷言", "治疗祷言 4级", "虚弱之触 6级", "虚弱妖术 6级", "噬灵瘟疫 6级", "神圣之火 8级", "精神鞭笞 6级", "暗影守卫 6级", "渐隐术 6级"}, +}, + +SHAMAN = { + [4] = {"地震术"}, + [6] = {"治疗波 2级", "地缚图腾"}, + [8] = {"闪电箭 2级", "石爪图腾", "地震术 2级", "闪电之盾"}, + [10] = {"烈焰震击", "火舌武器", "大地之力图腾"}, + [12] = {"净化术", "火焰新星图腾", "先祖之魂", "治疗波 3级"}, + [14] = {"闪电箭 3级", "地震术 3级", "石肤图腾 2级"}, + [16] = {"闪电之盾 2级", "石化武器 3级", "消毒术"}, + [18] = {"烈焰震击 2级", "火舌武器 2级", "石爪图腾 2级", "治疗波 4级", "战栗图腾"}, + [20] = {"闪电箭 4级", "灼热图腾 2级", "冰霜震击", "幽魂之狼", "次级治疗波"}, + [22] = {"火焰新星图腾 2级", "水下呼吸", "祛病术", "清毒图腾"}, + [24] = {"净化术 2级", "地震术 4级", "石肤图腾 3级", "石化武器 4级", "大地之力图腾 2级", "闪电之盾 3级", "抗寒图腾", "先祖之魂 2级"}, + [26] = {"闪电箭 5级", "熔岩图腾", "火舌武器 3级", "视界术", "法力之泉图腾"}, + [28] = {"石爪图腾 3级", "烈焰震击 3级", "冰封武器 2级", "抗火图腾", "火舌图腾", "水上行走", "次级治疗波 2级"}, + [30] = {"灼热图腾 3级", "星界传送", "根基图腾", "石化武器 5级", "风怒武器", "自然抗性图腾", "治疗之泉图腾 2级"}, + [32] = {"闪电箭 6级", "火焰新星图腾 3级", "闪电之盾 4级", "治疗波 6级", "闪电链", "风怒图腾"}, + [34] = {"冰霜震击 2级", "石肤图腾 4级", "岗哨图腾"}, + [36] = {"地震术 5级", "熔岩图腾 2级", "火舌武器 4级", "法力之泉图腾 2级", "次级治疗波 3级", "风墙图腾"}, + [38] = {"石爪图腾 4级", "冰封武器 3级", "抗寒图腾 2级", "大地之力图腾 3级", "火舌图腾 2级"}, + [40] = {"闪电箭 8级", "闪电链 2级", "烈焰震击 4级", "石肤图腾 5级", "治疗波 7级", "治疗链", "治疗之泉图腾 3级", "风怒武器 2级"}, + [42] = {"火焰新星图腾 4级", "灼热图腾 4级", "抗火图腾 2级", "风之优雅图腾"}, + [44] = {"闪电之盾 6级", "石化武器 6级", "冰霜震击 3级", "熔岩图腾 3级", "自然抗性图腾 2级", "风墙图腾 2级"}, + [46] = {"火舌武器 5级", "治疗链 2级"}, + [48] = {"地震术 6级", "石爪图腾 5级", "抗寒图腾 3级", "火舌图腾 3级", "治疗波 8级"}, + [50] = {"闪电箭 9级", "灼热图腾 5级", "治疗之泉图腾 4级", "风怒武器 3级", "宁静之风图腾"}, + [52] = {"烈焰震击 5级", "大地之力图腾 4级", "风怒图腾 3级", "次级治疗波 5级"}, + [54] = {"石化武器 7级", "石肤图腾 6级", "抗寒图腾 3级"}, + [56] = {"闪电箭 10级", "闪电链 4级", "熔岩图腾 4级", "冰封武器 4级", "抗火图腾 3级", "火舌图腾 4级", "风之优雅图腾 2级", "风墙图腾 3级", "治疗波 9级", "法力之泉图腾 4级"}, + [58] = {"冰霜震击 4级"}, + [60] = {"灼热图腾 6级", "风怒武器 4级", "自然抗性图腾 3级", "次级治疗波 6级", "治疗之泉图腾 5级"}, +}, diff --git a/agent-tools/class_trainer_skills_output.lua b/agent-tools/class_trainer_skills_output.lua new file mode 100644 index 0000000..187bcd2 --- /dev/null +++ b/agent-tools/class_trainer_skills_output.lua @@ -0,0 +1,63 @@ +WARRIOR = { + [4] = {"冲锋", "撕裂"}, + [6] = {"雷霆一击"}, + [8] = {"英勇打击 2级", "断筋"}, + [10] = {"撕裂 2级", "血性狂暴"}, + [12] = {"压制", "盾击", "战斗怒吼 2级"}, + [14] = {"挫志怒吼", "复仇"}, + [16] = {"英勇打击 3级", "惩戒痛击", "盾牌格挡"}, + [18] = {"雷霆一击 2级", "缴械"}, + [20] = {"撕裂 3级", "反击风暴", "顺劈斩"}, + [22] = {"战斗怒吼 3级", "破甲攻击 2级", "破胆怒吼"}, + [24] = {"英勇打击 4级", "挫志怒吼 2级", "复仇 2级", "斩杀"}, + [26] = {"冲锋 2级", "惩戒痛击 2级", "挑战怒吼"}, + [28] = {"雷霆一击 3级", "压制 2级", "盾墙"}, + [30] = {"撕裂 4级", "顺劈斩 2级"}, + [32] = {"英勇打击 5级", "断筋 2级", "斩杀 2级", "战斗怒吼 4级", "盾击 2级", "狂暴之怒"}, + [34] = {"挫志怒吼 3级", "复仇 3级", "破甲攻击 3级"}, + [36] = {"惩戒痛击 3级", "旋风斩"}, + [38] = {"雷霆一击 4级", "猛击 2级", "拳击"}, + [40] = {"英勇打击 6级", "撕裂 5级", "顺劈斩 3级", "斩杀 3级"}, + [42] = {"战斗怒吼 5级", "拦截 2级"}, + [44] = {"压制 3级", "挫志怒吼 4级", "复仇 4级"}, + [46] = {"冲锋 3级", "惩戒痛击 4级", "猛击 3级", "破甲攻击 4级"}, + [48] = {"英勇打击 7级", "雷霆一击 5级", "斩杀 4级"}, + [50] = {"撕裂 6级", "鲁莽", "顺劈斩 4级"}, + [52] = {"战斗怒吼 6级", "拦截 3级", "盾击 3级"}, + [54] = {"断筋 3级", "挫志怒吼 5级", "猛击 4级", "复仇 5级"}, + [56] = {"英勇打击 8级", "惩戒痛击 5级", "斩杀 5级"}, + [58] = {"雷霆一击 6级", "拳击 2级", "破甲攻击 5级"}, + [60] = {"撕裂 7级", "压制 4级", "顺劈斩 5级"}, +}, + +PALADIN = { + [4] = {"力量祝福", "审判"}, + [6] = {"圣光术 2级", "圣佑术", "十字军圣印"}, + [8] = {"纯净术", "制裁之锤"}, + [10] = {"圣疗术", "正义圣印 2级", "虔诚光环 2级", "保护祝福"}, + [12] = {"力量祝福 2级", "十字军圣印 2级"}, + [14] = {"圣光术 3级"}, + [16] = {"正义之怒", "惩罚光环"}, + [18] = {"正义圣印 3级", "圣佑术 2级"}, + [20] = {"驱邪术", "圣光闪现", "虔诚光环 3级"}, + [22] = {"圣光术 4级", "专注光环", "公正圣印", "力量祝福 3级", "十字军圣印 3级"}, + [24] = {"超度亡灵", "救赎 2级", "智慧祝福 2级", "制裁之锤 2级", "保护祝福 2级"}, + [26] = {"圣光闪现 2级", "正义圣印 4级", "拯救祝福", "惩罚光环 2级"}, + [28] = {"驱邪术 2级"}, + [30] = {"圣疗术 2级", "圣光术 5级", "光明圣印", "虔诚光环 4级", "神圣干涉"}, + [32] = {"冰霜抗性光环", "力量祝福 4级", "十字军圣印 4级"}, + [34] = {"智慧祝福 3级", "圣光闪现 3级", "正义圣印 5级", "圣盾术"}, + [36] = {"驱邪术 3级", "救赎 3级", "火焰抗性光环", "惩罚光环 3级"}, + [38] = {"圣光术 6级", "超度亡灵 2级", "智慧圣印", "保护祝福 3级"}, + [40] = {"光明祝福", "光明圣印 2级", "虔诚光环 5级", "制裁之锤 3级", "暗影抗性光环 2级", "命令圣印 3级"}, + [42] = {"圣光闪现 4级", "正义圣印 6级", "力量祝福 5级", "十字军圣印 5级"}, + [44] = {"驱邪术 4级", "智慧祝福 4级", "冰霜抗性光环 2级"}, + [46] = {"圣光术 7级", "惩罚光环 4级"}, + [48] = {"救赎 4级", "智慧圣印 2级", "火焰抗性光环 2级"}, + [50] = {"圣疗术 3级", "圣光闪现 5级", "光明祝福 2级", "光明圣印 3级", "正义圣印 7级", "虔诚光环 6级", "圣盾术 2级", "庇护祝福 3级", "命令圣印 4级"}, + [52] = {"驱邪术 5级", "超度亡灵 3级", "愤怒之锤 2级", "暗影抗性光环 3级", "力量祝福 6级", "十字军圣印 6级", "强效力量祝福"}, + [54] = {"圣光术 8级", "智慧祝福 5级", "强效智慧祝福", "制裁之锤 4级", "牺牲祝福 2级"}, + [56] = {"冰霜抗性光环 3级", "惩罚光环 5级"}, + [58] = {"圣光闪现 6级", "智慧圣印 3级", "正义圣印 8级"}, + [60] = {"驱邪术 6级", "神圣愤怒 2级", "救赎 5级", "光明祝福 3级", "光明圣印 4级", "愤怒之锤 3级", "强效光明祝福", "强效智慧祝福 2级", "虔诚光环 7级", "火焰抗性光环 3级", "庇护祝福 4级", "命令圣印 5级", "强效力量祝福 2级", "强效拯救祝福", "强效王者祝福", "强效庇护祝福"}, +}, \ No newline at end of file diff --git a/agent-tools/parse_skills.py b/agent-tools/parse_skills.py new file mode 100644 index 0000000..6c0b6bb --- /dev/null +++ b/agent-tools/parse_skills.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +import re + +def parse_file(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + result = {} + # Split by "需要等级" to find each skill block - the skill name is before it + parts = re.split(r'(需要等级\s+\d+)', content) + + for i in range(1, len(parts), 2): + if i+1 >= len(parts): + break + level_line = parts[i] # "需要等级 12" + level_match = re.search(r'需要等级\s+(\d+)', level_line) + if not level_match: + continue + level = int(level_match.group(1)) + if level % 2 != 0: + continue + + after = parts[i+1] + learn_match = re.search(r'学习:\s*(.+?)(?:\n|$)', after) + if not learn_match or '默认开启' in learn_match.group(1): + continue + + # Get skill name from the part before "需要等级" + before = parts[i-1] + lines = before.strip().split('\n') + skill_line = None + for line in reversed(lines): + line = line.strip() + if not line or 'javascript' in line or line.startswith('['): + continue + if re.match(r'^[\u4e00-\u9fff\s:]+(等级\s+\d+)?\s*$', line) and len(line) < 50: + skill_line = line + break + if not skill_line: + continue + + rank_match = re.match(r'^(.+?)\s+等级\s+(\d+)\s*$', skill_line) + if rank_match: + skill_base = rank_match.group(1).strip() + rank = int(rank_match.group(2)) + display = skill_base if rank == 1 else f"{skill_base} {rank}级" + else: + display = skill_line + + if any(x in display for x in ['魔兽世界', '职业', '需要', '·']): + continue + + if level not in result: + result[level] = [] + if display not in result[level]: + result[level].append(display) + + return dict(sorted(result.items())) + +def format_lua(data, name): + lines = [f"{name} = {{"] + for level, skills in sorted(data.items()): + skills_str = ", ".join(f'"{s}"' for s in sorted(skills)) + lines.append(f" [{level}] = {{{skills_str}}},") + lines.append("},") + return "\n".join(lines) + +priest_file = r'C:\Users\rucky\.cursor\projects\e-Game-trutle-wow-Interface-AddOns-Nanami-UI\agent-tools\8aaa3634-8b06-4c6a-838c-18f248f9b747.txt' +shaman_file = r'C:\Users\rucky\.cursor\projects\e-Game-trutle-wow-Interface-AddOns-Nanami-UI\agent-tools\e5e3b3f9-edfa-4a99-a95e-c9f7ed954371.txt' + +priest_data = parse_file(priest_file) +shaman_data = parse_file(shaman_file) + +output = format_lua(priest_data, "PRIEST") + "\n\n" + format_lua(shaman_data, "SHAMAN") +outpath = r'C:\Users\rucky\.cursor\projects\e-Game-trutle-wow-Interface-AddOns-Nanami-UI\agent-tools\class_trainer_skills.lua' +with open(outpath, 'w', encoding='utf-8') as f: + f.write(output) +print("Done. Output written to class_trainer_skills.lua") diff --git a/img/UI-Classes-Circles.tga b/img/UI-Classes-Circles.tga new file mode 100644 index 0000000..542d0f7 Binary files /dev/null and b/img/UI-Classes-Circles.tga differ diff --git a/img/cat.tga b/img/cat.tga new file mode 100644 index 0000000..c697d9b Binary files /dev/null and b/img/cat.tga differ diff --git a/img/df-gryphon-beta.tga b/img/df-gryphon-beta.tga new file mode 100644 index 0000000..bb4262f Binary files /dev/null and b/img/df-gryphon-beta.tga differ diff --git a/img/df-gryphon.tga b/img/df-gryphon.tga new file mode 100644 index 0000000..6c7e965 Binary files /dev/null and b/img/df-gryphon.tga differ diff --git a/img/df-wyvern.tga b/img/df-wyvern.tga new file mode 100644 index 0000000..4414fed Binary files /dev/null and b/img/df-wyvern.tga differ diff --git a/img/dly.tga b/img/dly.tga new file mode 100644 index 0000000..4bfcf76 Binary files /dev/null and b/img/dly.tga differ diff --git a/img/fs.tga b/img/fs.tga new file mode 100644 index 0000000..d2c3e0b Binary files /dev/null and b/img/fs.tga differ diff --git a/img/gude-2026-03-05.log b/img/gude-2026-03-05.log new file mode 100644 index 0000000..b1c716d --- /dev/null +++ b/img/gude-2026-03-05.log @@ -0,0 +1,3 @@ +12:40:26:562 [CRITICAL] SharedConnection - Failed to open WinHTTP Session. 参数错误。 +, last error code = 87 +12:40:26:562 [ERROR] Failed to Configure Secure Protocols. WinHTTP Incorrect Handle Type. last error code = 12018 diff --git a/img/icon.tga b/img/icon.tga new file mode 100644 index 0000000..f87382b Binary files /dev/null and b/img/icon.tga differ diff --git a/img/icon2.tga b/img/icon2.tga new file mode 100644 index 0000000..fbbac32 Binary files /dev/null and b/img/icon2.tga differ diff --git a/img/icon3.tga b/img/icon3.tga new file mode 100644 index 0000000..cd33663 Binary files /dev/null and b/img/icon3.tga differ diff --git a/img/icon4.tga b/img/icon4.tga new file mode 100644 index 0000000..012a1ba Binary files /dev/null and b/img/icon4.tga differ diff --git a/img/icon5.tga b/img/icon5.tga new file mode 100644 index 0000000..7c0ea43 Binary files /dev/null and b/img/icon5.tga differ diff --git a/img/icon6.tga b/img/icon6.tga new file mode 100644 index 0000000..ada7d05 Binary files /dev/null and b/img/icon6.tga differ diff --git a/img/icon7.tga b/img/icon7.tga new file mode 100644 index 0000000..f44d9c2 Binary files /dev/null and b/img/icon7.tga differ diff --git a/img/icon8.tga b/img/icon8.tga new file mode 100644 index 0000000..065cd4e Binary files /dev/null and b/img/icon8.tga differ diff --git a/img/lr.tga b/img/lr.tga new file mode 100644 index 0000000..d89bf51 Binary files /dev/null and b/img/lr.tga differ diff --git a/img/map.tga b/img/map.tga new file mode 100644 index 0000000..43f1950 Binary files /dev/null and b/img/map.tga differ diff --git a/img/ms.tga b/img/ms.tga new file mode 100644 index 0000000..97b8861 Binary files /dev/null and b/img/ms.tga differ diff --git a/img/qs.tga b/img/qs.tga new file mode 100644 index 0000000..df18070 Binary files /dev/null and b/img/qs.tga differ diff --git a/img/qxz.tga b/img/qxz.tga new file mode 100644 index 0000000..b80a448 Binary files /dev/null and b/img/qxz.tga differ diff --git a/img/sm.tga b/img/sm.tga new file mode 100644 index 0000000..0fb26d5 Binary files /dev/null and b/img/sm.tga differ diff --git a/img/ss.tga b/img/ss.tga new file mode 100644 index 0000000..691c97d Binary files /dev/null and b/img/ss.tga differ diff --git a/img/zs.tga b/img/zs.tga new file mode 100644 index 0000000..06e6873 Binary files /dev/null and b/img/zs.tga differ diff --git a/img/魔兽插件图标含义对照表.html b/img/魔兽插件图标含义对照表.html new file mode 100644 index 0000000..93cdb09 --- /dev/null +++ b/img/魔兽插件图标含义对照表.html @@ -0,0 +1,177 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ABCDEFGHI
+
1
+
行 \ 列12345678
+
2
+
第 1 行🐾 主题logo💾 保存/配置文件❌ 关闭/取消🔗 断开/解除绑定❗ 任务/重要警告🦁 联盟阵营👹 部落阵营🧝 角色/玩家属性
+
3
+
第 2 行💬 聊天/社交⚙️ 设置/选项🧠 智力/宏命令🎒 背包/行囊🐴 坐骑🏆 成就系统💰 金币/经济👥 好友/社交列表
+
4
+
第 3 行🚪 离开/退出队伍🫂 公会/团队💰 战利品/拾取🐉 首领/地下城⚒️ 专业技能/制造⏻ 退出游戏/系统🗺️ 世界地图/导航🌳 天赋树/专精
+
5
+
第 4 行🔥 法术/魔法🗡️ 攻击/近战📈 伤害统计/数据📡 网络延迟 (Ping)✉️ 邮件/信箱📜 任务日志📖 法术书/技能🏪 商店/商人
+
6
+
第 5 行⭐ 收藏/团队标记🧪 治疗药水/消耗品☠️ 死亡/骷髅标记➕ 治疗/生命值🏠 房屋/要塞/旅店📜 笔记/文本卷轴🌿 草药学/自然🗝️ 钥匙/大秘境
+
7
+
第 6 行🛡️ 坦克/防御🔨 拍卖行/竞标🎣 钓鱼专业📅 日历/游戏活动🏰 副本入口/传送门🔠 寻找组队 (LFG)📋 角色面板/纸娃娃❓ 帮助/客服/未知
+
8
+
第 7 行🔊 声音/音量设置🔍 搜索/观察玩家🌿 荣誉/PVP/威望🔠 菜单/列表项🛒 游戏商城/购买💪 力量/增益 (Buff)🏹 远程/猎人武器🥾 移动速度/敏捷
+
9
+
第 8 行⚡ 能量/急速/闪电🧪 毒药/有害效果🛡️ 护甲提升/减伤🥣 研磨/炼金术🔥 营火/烹饪专业⛺ 营地/休息区🌀 炉石⛏️ 采矿专业
+
\ No newline at end of file diff --git a/乌龟服职业属性收益研究.pdf b/乌龟服职业属性收益研究.pdf new file mode 100644 index 0000000..ff6a2ac Binary files /dev/null and b/乌龟服职业属性收益研究.pdf differ diff --git a/乌龟服魔兽世界 (Turtle WoW) 深度理论研究与属性收益终极报告.pdf b/乌龟服魔兽世界 (Turtle WoW) 深度理论研究与属性收益终极报告.pdf new file mode 100644 index 0000000..eab7597 Binary files /dev/null and b/乌龟服魔兽世界 (Turtle WoW) 深度理论研究与属性收益终极报告.pdf differ