-------------------------------------------------------------------------------- -- Nanami-UI: Beast Training UI (BeastTrainingUI.lua) -- Dedicated panel for Hunter pet beast training (训练野兽) -- Separate from TradeSkillUI; shows training points, skill ranks, etc. -- NOTE: Lua 5.0 upvalue limit = 32 per closure; all state packed into tables. -------------------------------------------------------------------------------- SFrames = SFrames or {} SFrames.BeastTrainingUI = {} local BTUI = SFrames.BeastTrainingUI SFramesDB = SFramesDB or {} local BEAST_NAMES = { ["训练野兽"] = true, ["Beast Training"] = true, } function BTUI.IsBeastTraining() local name = GetCraftName and GetCraftName() or "" return BEAST_NAMES[name] == true end -------------------------------------------------------------------------------- -- Theme -------------------------------------------------------------------------------- local T = SFrames.Theme:Extend({ tpGood = { 0.40, 0.90, 0.40 }, tpLow = { 0.90, 0.60, 0.20 }, tpNone = { 0.90, 0.30, 0.30 }, rankText = { 0.80, 0.70, 1.00 }, available = { 0.25, 1.00, 0.25 }, unavailable = { 0.80, 0.20, 0.20 }, learned = { 0.50, 0.50, 0.50 }, }) -------------------------------------------------------------------------------- -- Layout (packed to save upvalues) -------------------------------------------------------------------------------- local L = { FRAME_W = 620, FRAME_H = 470, HEADER_H = 52, SIDE_PAD = 10, FILTER_H = 26, LIST_ROW_H = 28, CAT_ROW_H = 20, BOTTOM_H = 44, SCROLL_STEP = 36, SCROLLBAR_W = 12, MAX_ROWS = 60, LEFT_W = 280, } 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 to save upvalues) -------------------------------------------------------------------------------- local S = { MainFrame = nil, selectedIndex = nil, currentFilter = "all", displayList = {}, rowButtons = {}, collapsedCats = {}, } -------------------------------------------------------------------------------- -- Tooltip scanner & extended craft info -------------------------------------------------------------------------------- local scanTip = nil function BTUI.GetCraftExtendedInfo(index) local name, rank, skillType, v4, _, tpCost, reqLevel = GetCraftInfo(index) local canLearn = (tonumber(reqLevel) or 0) > 0 tpCost = tonumber(tpCost) or 0 return name, rank, skillType, canLearn, tpCost end function BTUI.GetSkillTooltipLines(index) if not scanTip then scanTip = CreateFrame("GameTooltip", "SFramesBTScanTip", nil, "GameTooltipTemplate") end scanTip:SetOwner(WorldFrame, "ANCHOR_NONE") scanTip:ClearLines() local ok = pcall(scanTip.SetCraftSpell, scanTip, index) if not ok then scanTip:Hide(); return {} end local lines = {} local numLines = scanTip:NumLines() for i = 2, numLines do local leftFS = _G["SFramesBTScanTipTextLeft" .. i] local rightFS = _G["SFramesBTScanTipTextRight" .. i] if leftFS then local lt = leftFS:GetText() or "" local rt = rightFS and rightFS:GetText() or "" local r, g, b = leftFS:GetTextColor() if lt ~= "" or rt ~= "" then table.insert(lines, { left = lt, right = rt, r = r, g = g, b = b, }) end end end scanTip:Hide() return lines end -------------------------------------------------------------------------------- -- Helpers -------------------------------------------------------------------------------- function BTUI.GetFont() if SFrames and SFrames.GetFont then return SFrames:GetFont() end return "Fonts\\ARIALN.TTF" end function BTUI.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 BTUI.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 -------------------------------------------------------------------------------- -- Widget Factories -------------------------------------------------------------------------------- function BTUI.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(BTUI.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 BTUI.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(BTUI.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 BTUI.CreateListRow(parent, idx) local row = CreateFrame("Button", nil, parent) row:SetWidth(L.LIST_ROW_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 font = BTUI.GetFont() local tpFS = row:CreateFontString(nil, "OVERLAY") tpFS:SetFont(font, 10, "OUTLINE") tpFS:SetPoint("RIGHT", row, "RIGHT", -4, 0) tpFS:SetJustifyH("RIGHT") tpFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) tpFS:Hide() row.tpFS = tpFS local nameFS = row:CreateFontString(nil, "OVERLAY") nameFS:SetFont(font, 11, "OUTLINE") nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 5, 2) nameFS:SetPoint("RIGHT", tpFS, "LEFT", -4, 0) nameFS:SetJustifyH("LEFT") nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) row.nameFS = nameFS local rankFS = row:CreateFontString(nil, "OVERLAY") rankFS:SetFont(font, 9, "OUTLINE") rankFS:SetPoint("TOPLEFT", nameFS, "BOTTOMLEFT", 0, 0) rankFS:SetJustifyH("LEFT") rankFS:SetTextColor(T.rankText[1], T.rankText[2], T.rankText[3]) row.rankFS = rankFS 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.craftIndex = nil; row.isHeader = false row:SetScript("OnEnter", function() if this.craftIndex and not this.isHeader then GameTooltip:SetOwner(this, "ANCHOR_RIGHT") local ok2 = pcall(GameTooltip.SetCraftSpell, GameTooltip, this.craftIndex) if ok2 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.rankFS:Hide(); self.tpFS: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 function row:SetAsSkill(skill) self.isHeader = false; self.craftIndex = skill.index self:SetHeight(L.LIST_ROW_H) self.iconFrame:Show(); self.nameFS:Show() self.catFS:Hide(); self.catSep:Hide(); self.highlight:SetAlpha(0.3) local iconTex = GetCraftIcon and GetCraftIcon(skill.index) self.icon:SetTexture(iconTex) self.nameFS:SetText(skill.name) if skill.rank and skill.rank ~= "" then self.rankFS:SetText(skill.rank) self.rankFS:Show() else self.rankFS:SetText("") self.rankFS:Hide() end if skill.canLearn then self.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3]) self.icon:SetVertexColor(1, 1, 1) local tp = skill.tpCost or 0 if tp > 0 then local remaining = BTUI.GetRemainingTP() if remaining >= tp then self.tpFS:SetTextColor(T.tpGood[1], T.tpGood[2], T.tpGood[3]) else self.tpFS:SetTextColor(T.tpNone[1], T.tpNone[2], T.tpNone[3]) end self.tpFS:SetText(tp .. " TP") self.tpFS:Show() else self.tpFS:Hide() end else self.nameFS:SetTextColor(T.learned[1], T.learned[2], T.learned[3]) self.icon:SetVertexColor(0.5, 0.5, 0.5) self.tpFS:Hide() end end function row:Clear() self.craftIndex = nil; self.isHeader = false self.selBg:Hide(); self.selGlow:Hide(); self.selTop:Hide(); self.selBot:Hide() self.tpFS:Hide() self:Hide() end return row end -------------------------------------------------------------------------------- -- Logic -------------------------------------------------------------------------------- function BTUI.BuildDisplayList() S.displayList = {} local numCrafts = GetNumCrafts and GetNumCrafts() or 0 if numCrafts == 0 then return end local currentCat = nil local catSkills = {} local catOrder = {} for i = 1, numCrafts do local name, rank, skillType, canLearn, tpCost = BTUI.GetCraftExtendedInfo(i) if name then if skillType == "header" then currentCat = name if not catSkills[name] then catSkills[name] = {} table.insert(catOrder, name) end else if not currentCat then currentCat = "技能" if not catSkills[currentCat] then catSkills[currentCat] = {} table.insert(catOrder, currentCat) end end local show = true if S.currentFilter == "available" then show = canLearn elseif S.currentFilter == "used" then show = (not canLearn) end if show then table.insert(catSkills[currentCat], { index = i, name = name, rank = rank or "", canLearn = canLearn, tpCost = tpCost, }) end end end end local hasCats = table.getn(catOrder) > 1 for _, catName in ipairs(catOrder) do local skills = catSkills[catName] if skills and table.getn(skills) > 0 then if hasCats then table.insert(S.displayList, { type = "header", name = catName, collapsed = S.collapsedCats[catName], }) end if not S.collapsedCats[catName] then for _, skill in ipairs(skills) do table.insert(S.displayList, { type = "skill", data = skill }) end end end end end function BTUI.GetRemainingTP() if not GetPetTrainingPoints then return 0, 0 end local ok, total, spent = pcall(GetPetTrainingPoints) if not ok then return 0, 0 end total = total or 0 spent = spent or 0 return total - spent, total end function BTUI.UpdateTrainingPoints() if not S.MainFrame then return end local remaining, total = BTUI.GetRemainingTP() local color = T.tpGood if remaining <= 0 then color = T.tpNone elseif remaining < 10 then color = T.tpLow end S.MainFrame.tpValueFS:SetText(tostring(remaining)) S.MainFrame.tpValueFS:SetTextColor(color[1], color[2], color[3]) end function BTUI.UpdateList() if not S.MainFrame or not S.MainFrame:IsVisible() then return end BTUI.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:Show(); y = y + L.CAT_ROW_H else row:SetAsSkill(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 BTUI.UpdateDetail() if not S.MainFrame then return end local detail = S.MainFrame.detail if not S.selectedIndex then detail.iconFrame:Hide() detail.nameFS:SetText(""); detail.rankFS:SetText("") detail.descFS:SetText(""); detail.costFS:SetText("") detail.reqFS:SetText("") S.MainFrame.trainBtn:SetDisabled(true) return end local name, rank, skillType, canLearn, tpCost = BTUI.GetCraftExtendedInfo(S.selectedIndex) local iconTex = GetCraftIcon and GetCraftIcon(S.selectedIndex) detail.icon:SetTexture(iconTex); detail.iconFrame:Show() detail.nameFS:SetText(name or "") if canLearn then detail.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3]) else detail.nameFS:SetTextColor(T.learned[1], T.learned[2], T.learned[3]) end if rank and rank ~= "" then detail.rankFS:SetText(rank) detail.rankFS:SetTextColor(T.rankText[1], T.rankText[2], T.rankText[3]) else detail.rankFS:SetText("") end local desc = "" if GetCraftDescription then local ok, d = pcall(GetCraftDescription, S.selectedIndex) if ok and d then desc = d end end detail.descFS:SetText(desc) local descH = detail.descFS:GetHeight() or 40 detail.descScroll:GetScrollChild():SetHeight(math.max(1, descH)) detail.descScroll:SetVerticalScroll(0) if tpCost > 0 then local remaining = BTUI.GetRemainingTP() local costColor = remaining >= tpCost and "|cff40ff40" or "|cffff4040" detail.costFS:SetText("训练点数: " .. costColor .. tpCost .. "|r (剩余: " .. remaining .. ")") elseif canLearn then detail.costFS:SetText("训练点数: |cff40ff40免费|r") else detail.costFS:SetText("") end local lines = BTUI.GetSkillTooltipLines(S.selectedIndex) local reqParts = {} for _, line in ipairs(lines) do if line.r and line.r > 0.85 and line.g < 0.35 and line.b < 0.35 then table.insert(reqParts, "|cffff4040" .. line.left .. "|r") end end detail.reqFS:SetText(table.concat(reqParts, "\n")) S.MainFrame.trainBtn:SetDisabled(not canLearn) end function BTUI.UpdateFilters() if not S.MainFrame then return end S.MainFrame.filterAll:SetActive(S.currentFilter == "all") S.MainFrame.filterAvail:SetActive(S.currentFilter == "available") S.MainFrame.filterUsed:SetActive(S.currentFilter == "used") end function BTUI.FullUpdate() BTUI.UpdateTrainingPoints() BTUI.UpdateFilters() BTUI.UpdateList() BTUI.UpdateDetail() if BTUI.UpdateScrollbar then BTUI.UpdateScrollbar() end end function BTUI.SelectSkill(index) S.selectedIndex = index if SelectCraft then pcall(SelectCraft, index) end BTUI.FullUpdate() end function BTUI.ToggleCategory(catName) if S.collapsedCats[catName] then S.collapsedCats[catName] = nil else S.collapsedCats[catName] = true end BTUI.FullUpdate() end -------------------------------------------------------------------------------- -- Hide Blizzard Craft Frame -------------------------------------------------------------------------------- function BTUI.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 -------------------------------------------------------------------------------- function BTUI:Initialize() if S.MainFrame then return end local MF = CreateFrame("Frame", "SFramesBeastTrainingFrame", 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) BTUI.SetRoundBackdrop(MF); BTUI.CreateShadow(MF) local font = BTUI.GetFont() -- ═══ Header ═════════════════════════════════════════════════════════ 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 titleIco = SFrames:CreateIcon(header, "dragon", 16) titleIco:SetDrawLayer("OVERLAY") titleIco:SetPoint("TOPLEFT", header, "TOPLEFT", L.SIDE_PAD, -6) 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:SetJustifyH("LEFT") titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) MF.titleFS = titleFS -- Training points in header local tpIco = SFrames:CreateIcon(header, "star", 12) tpIco:SetDrawLayer("OVERLAY") tpIco:SetPoint("BOTTOMLEFT", header, "BOTTOMLEFT", L.SIDE_PAD, 8) tpIco:SetVertexColor(0.55, 0.85, 0.4) local tpLabelFS = header:CreateFontString(nil, "OVERLAY") tpLabelFS:SetFont(font, 11, "OUTLINE") tpLabelFS:SetPoint("LEFT", tpIco, "RIGHT", 4, 0) tpLabelFS:SetText("训练点数:") tpLabelFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) local tpValueFS = header:CreateFontString(nil, "OVERLAY") tpValueFS:SetFont(font, 13, "OUTLINE") tpValueFS:SetPoint("LEFT", tpLabelFS, "RIGHT", 6, 0) tpValueFS:SetText("0") tpValueFS:SetTextColor(T.tpGood[1], T.tpGood[2], T.tpGood[3]) MF.tpValueFS = tpValueFS -- Pet level in header local petLvlFS = header:CreateFontString(nil, "OVERLAY") petLvlFS:SetFont(font, 10, "OUTLINE") petLvlFS:SetPoint("LEFT", tpValueFS, "RIGHT", 16, 0) petLvlFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) MF.petLvlFS = petLvlFS -- Close button 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: Filters + Skill 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 = BTUI.CreateFilterBtn(fb, "全部", 52) fAll:SetPoint("LEFT", fb, "LEFT", 0, 0) fAll:SetScript("OnClick", function() S.currentFilter = "all"; BTUI.FullUpdate() end) MF.filterAll = fAll local fAvail = BTUI.CreateFilterBtn(fb, "可学", 52) fAvail:SetPoint("LEFT", fAll, "RIGHT", 3, 0) fAvail:SetScript("OnClick", function() S.currentFilter = "available"; BTUI.FullUpdate() end) MF.filterAvail = fAvail local fUsed = BTUI.CreateFilterBtn(fb, "已学", 52) fUsed:SetPoint("LEFT", fAvail, "RIGHT", 3, 0) fUsed:SetScript("OnClick", function() S.currentFilter = "used"; BTUI.FullUpdate() end) MF.filterUsed = fUsed -- List scroll area local listTop = L.HEADER_H + L.FILTER_H + 8 local ls = CreateFrame("ScrollFrame", "SFramesBTListScroll", 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", "SFramesBTListContent", ls) lc:SetWidth(L.LIST_ROW_W); lc:SetHeight(1) ls:SetScrollChild(lc) ls:EnableMouseWheel(true) -- Scrollbar 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) 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 = BTUI.GetScrollMax() local newScroll = this._startScroll + (delta / trackH) * scrollMax newScroll = math.max(0, math.min(scrollMax, newScroll)) ls:SetVerticalScroll(newScroll) BTUI.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 * BTUI.GetScrollMax()) BTUI.UpdateScrollbar() end) MF.sbTrack = sbTrack; MF.sbThumb = sbThumb function BTUI.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 BTUI.UpdateScrollbar() if not MF.sbThumb or not MF.sbTrack then return end local scrollMax = BTUI.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 = BTUI.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 BTUI.UpdateScrollbar() end) ls:SetScript("OnScrollRangeChanged", function() BTUI.UpdateScrollbar() end) ls.content = lc; MF.listScroll = ls for i = 1, L.MAX_ROWS do local row = BTUI.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 = BTUI.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 BTUI.UpdateScrollbar() end) row:SetScript("OnClick", function() if this.isHeader and this.catName then BTUI.ToggleCategory(this.catName) elseif this.craftIndex then BTUI.SelectSkill(this.craftIndex) end end) S.rowButtons[i] = row end -- ═══ RIGHT: Detail ══════════════════════════════════════════════════ local rightX = L.LEFT_W + L.SIDE_PAD 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 -- Icon local dIF = CreateFrame("Frame", nil, det) dIF:SetWidth(44); dIF:SetHeight(44); 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 dIF:EnableMouse(true) dIF:SetScript("OnEnter", function() if S.selectedIndex then GameTooltip:SetOwner(this, "ANCHOR_RIGHT") local ok2 = pcall(GameTooltip.SetCraftSpell, GameTooltip, S.selectedIndex) if ok2 then GameTooltip:Show() else GameTooltip:Hide() end end end) dIF:SetScript("OnLeave", function() GameTooltip:Hide() end) -- Name local dName = det:CreateFontString(nil, "OVERLAY") dName:SetFont(font, 14, "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 -- Rank local dRank = det:CreateFontString(nil, "OVERLAY") dRank:SetFont(font, 12, "OUTLINE") dRank:SetPoint("TOPLEFT", dName, "BOTTOMLEFT", 0, -2) dRank:SetJustifyH("LEFT") dRank:SetTextColor(T.rankText[1], T.rankText[2], T.rankText[3]) det.rankFS = dRank -- Separator local sep1 = det:CreateTexture(nil, "ARTWORK") sep1:SetTexture("Interface\\Buttons\\WHITE8X8"); sep1:SetHeight(1) sep1:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -54) sep1:SetPoint("RIGHT", det, "RIGHT", 0, 0) sep1:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) -- Description (scrollable) local descScroll = CreateFrame("ScrollFrame", nil, det) descScroll:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -60) descScroll:SetPoint("RIGHT", det, "RIGHT", -4, 0) descScroll:SetHeight(120) 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) det.descScroll = descScroll local descContent = CreateFrame("Frame", nil, descScroll) descContent:SetWidth(L.CONTENT_W - 8); descContent:SetHeight(1) descScroll:SetScrollChild(descContent) local dDesc = descContent:CreateFontString(nil, "OVERLAY") dDesc:SetFont(font, 11) dDesc:SetPoint("TOPLEFT", descContent, "TOPLEFT", 0, 0) dDesc:SetWidth(L.CONTENT_W - 8) dDesc:SetJustifyH("LEFT") dDesc:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) det.descFS = dDesc -- Separator 2 local sep2 = det:CreateTexture(nil, "ARTWORK") sep2:SetTexture("Interface\\Buttons\\WHITE8X8"); sep2:SetHeight(1) sep2:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -186) sep2:SetPoint("RIGHT", det, "RIGHT", 0, 0) sep2:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) -- TP Cost local dCost = det:CreateFontString(nil, "OVERLAY") dCost:SetFont(font, 12, "OUTLINE") dCost:SetPoint("TOPLEFT", det, "TOPLEFT", 2, -194) dCost:SetPoint("RIGHT", det, "RIGHT", -4, 0) dCost:SetJustifyH("LEFT") dCost:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) det.costFS = dCost -- Requirements local dReq = det:CreateFontString(nil, "OVERLAY") dReq:SetFont(font, 11, "OUTLINE") dReq:SetPoint("TOPLEFT", dCost, "BOTTOMLEFT", 0, -4) dReq:SetPoint("RIGHT", det, "RIGHT", -4, 0) dReq:SetJustifyH("LEFT") det.reqFS = dReq -- ═══ Bottom Bar ═════════════════════════════════════════════════════ local bsep = MF:CreateTexture(nil, "ARTWORK") bsep:SetTexture("Interface\\Buttons\\WHITE8X8"); bsep:SetHeight(1) bsep:SetPoint("BOTTOMLEFT", MF, "BOTTOMLEFT", 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]) local trainBtn = BTUI.CreateActionBtn(MF, "训练", 100) trainBtn:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -L.SIDE_PAD, 8) trainBtn:SetScript("OnClick", function() if this.disabled then return end if S.selectedIndex and DoCraft then DoCraft(S.selectedIndex) BTUI.FullUpdate() if not MF._refreshFrame then MF._refreshFrame = CreateFrame("Frame") end MF._refreshElapsed = 0 MF._refreshCount = 0 MF._refreshFrame:SetScript("OnUpdate", function() MF._refreshElapsed = (MF._refreshElapsed or 0) + arg1 if MF._refreshElapsed >= 0.2 then MF._refreshElapsed = 0 MF._refreshCount = (MF._refreshCount or 0) + 1 BTUI.FullUpdate() if MF._refreshCount >= 3 then MF._refreshFrame:SetScript("OnUpdate", nil) end end end) end end) MF.trainBtn = trainBtn local closeB = BTUI.CreateActionBtn(MF, "关闭", 80) closeB:SetPoint("BOTTOMRIGHT", trainBtn, "BOTTOMLEFT", -6, 0) closeB:SetScript("OnClick", function() S.MainFrame:Hide() end) -- ═══ Events ═════════════════════════════════════════════════════════ MF:SetScript("OnHide", function() if CloseCraft then pcall(CloseCraft) end BTUI.CleanupBlizzardCraft() end) MF:RegisterEvent("CRAFT_SHOW") MF:RegisterEvent("CRAFT_UPDATE") MF:RegisterEvent("CRAFT_CLOSE") MF:SetScript("OnEvent", function() if event == "CRAFT_SHOW" then if not BTUI.IsBeastTraining() then return end if CraftFrame then CraftFrame:SetScript("OnHide", function() end) CraftFrame:SetAlpha(0); CraftFrame:EnableMouse(false) end S.selectedIndex = nil; S.currentFilter = "all"; S.collapsedCats = {} local petName = UnitName("pet") or "" if petName ~= "" then MF.titleFS:SetText("训练野兽 - " .. petName) else MF.titleFS:SetText("训练野兽") end local petLvl = UnitLevel("pet") if petLvl and petLvl > 0 then MF.petLvlFS:SetText("宠物等级: " .. petLvl) else MF.petLvlFS:SetText("") end MF:Show() BTUI.BuildDisplayList() for _, entry in ipairs(S.displayList) do if entry.type == "skill" then BTUI.SelectSkill(entry.data.index); break end end BTUI.FullUpdate() MF._hideBlizzTimer = 0 MF: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) BTUI.CleanupBlizzardCraft() end end) elseif event == "CRAFT_UPDATE" then if S.MainFrame:IsVisible() then BTUI.FullUpdate() end elseif event == "CRAFT_CLOSE" then if S.MainFrame:IsVisible() then BTUI.CleanupBlizzardCraft() S.MainFrame._hideBlizzTimer = nil S.MainFrame:SetScript("OnUpdate", nil) S.MainFrame:Hide() end end end) MF:Hide() tinsert(UISpecialFrames, "SFramesBeastTrainingFrame") end -------------------------------------------------------------------------------- -- Bootstrap -------------------------------------------------------------------------------- local bootstrap = CreateFrame("Frame") bootstrap:RegisterEvent("PLAYER_LOGIN") bootstrap:SetScript("OnEvent", function() if event == "PLAYER_LOGIN" then if SFramesDB.enableTradeSkill == nil then SFramesDB.enableTradeSkill = true end if SFramesDB.enableTradeSkill ~= false then BTUI:Initialize() end end end) SLASH_BTDEBUG1 = "/btdebug" SlashCmdList["BTDEBUG"] = function() local p = "|cffff80ff[BT-Debug]|r " local numCrafts = GetNumCrafts and GetNumCrafts() or 0 DEFAULT_CHAT_FRAME:AddMessage(p .. "Total crafts: " .. numCrafts .. " TP: " .. BTUI.GetRemainingTP()) local shown = 0 for i = 1, numCrafts do local v1,v2,v3,v4,v5,v6,v7 = GetCraftInfo(i) if v1 and v3 ~= "header" then shown = shown + 1 if shown <= 8 then if SelectCraft then pcall(SelectCraft, i) end local nr = GetCraftNumReagents and GetCraftNumReagents(i) or 0 local reagentCost = "" if nr and nr > 0 then for r = 1, nr do local rn, rt, rc, pc = GetCraftReagentInfo(i, r) reagentCost = reagentCost .. " [" .. tostring(rn) .. "x" .. tostring(rc) .. "]" end end DEFAULT_CHAT_FRAME:AddMessage(p .. i .. ": " .. tostring(v1) .. " " .. tostring(v2) .. " v7=" .. tostring(v7) .. " reagents=" .. tostring(nr) .. reagentCost) end end end DEFAULT_CHAT_FRAME:AddMessage(p .. "Total skills: " .. shown) end