diff --git a/AFKScreen.lua b/AFKScreen.lua index 2c330d0..08e3316 100644 --- a/AFKScreen.lua +++ b/AFKScreen.lua @@ -110,6 +110,22 @@ end function AFK:Build() if self.frame then return end + local existing = getglobal("NanamiAFKScreen") + if existing then + existing:SetScript("OnUpdate", nil) + existing:SetScript("OnKeyDown", nil) + existing:SetScript("OnMouseDown", nil) + existing:EnableKeyboard(false) + existing:EnableMouse(false) + existing:Hide() + end + + local existingWatcher = getglobal("NanamiAFKWatcher") + if existingWatcher then + existingWatcher:SetScript("OnUpdate", nil) + existingWatcher:Hide() + end + local f = CreateFrame("Frame", "NanamiAFKScreen", WorldFrame) f:SetFrameStrata("FULLSCREEN_DIALOG") f:SetFrameLevel(100) @@ -947,7 +963,7 @@ function AFK:ScanWorldBuffs() local matched = {} if not self._buffTip then - self._buffTip = CreateFrame("GameTooltip", "NanamiAFKBuffTip", WorldFrame, "GameTooltipTemplate") + self._buffTip = getglobal("NanamiAFKBuffTip") or CreateFrame("GameTooltip", "NanamiAFKBuffTip", UIParent, "GameTooltipTemplate") end -- Build texture → remaining time mapping @@ -1149,7 +1165,7 @@ end -------------------------------------------------------------------------------- function AFK:OnUpdate(elapsed) - if not elapsed then return end + if not elapsed or not self.frame then return end -- Fade logic if self.fadeDirection then @@ -1209,6 +1225,7 @@ function AFK:OnUpdate(elapsed) -- Particle animation local now = GetTime() + if not self.particles then return end local sw = self.frame:GetWidth() local sh = self.frame:GetHeight() if sw < 100 then sw = 1024 end @@ -1329,6 +1346,39 @@ function AFK:ForceHide() UIParent:Show() end +function AFK:Cleanup() + if self.model and self.model.ClearModel then + pcall(function() self.model:ClearModel() end) + end + + if self.frame then + self.frame:SetScript("OnUpdate", nil) + self.frame:SetScript("OnKeyDown", nil) + self.frame:SetScript("OnMouseDown", nil) + self.frame:EnableKeyboard(false) + self.frame:EnableMouse(false) + self.frame:Hide() + self.frame:SetAlpha(0) + end + + local watcher = getglobal("NanamiAFKWatcher") + if watcher then + watcher:SetScript("OnUpdate", nil) + watcher:Hide() + end + + if self._buffTip then + self._buffTip:Hide() + end + + self.isShowing = false + self.fadeDirection = nil + self._exiting = false + self._danceWait = nil + + UIParent:Show() +end + function AFK:RequestExit() if self._exiting then return end self:Hide() @@ -1413,6 +1463,9 @@ end -------------------------------------------------------------------------------- function AFK:Initialize() + UIParent:Show() + self:Cleanup() + self:Build() self._isAFK = false self._lastActivity = GetTime() @@ -1421,30 +1474,32 @@ function AFK:Initialize() AFK._lastActivity = GetTime() end - -- Hook action bar usage - local origUseAction = UseAction - UseAction = function(a1, a2, a3) - MarkActive() - return origUseAction(a1, a2, a3) - end + if not self._hooked then + self._hooked = true - if CastSpellByName then - local origCast = CastSpellByName - CastSpellByName = function(a1, a2) + local origUseAction = UseAction + UseAction = function(a1, a2, a3) MarkActive() - return origCast(a1, a2) + 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 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", @@ -1457,17 +1512,16 @@ function AFK:Initialize() SFrames:RegisterEvent(ev, function() AFK:ResetIdleTimer() end) end - -- Watcher frame: tracks cursor movement + checks idle time - local watcher = CreateFrame("Frame", "NanamiAFKWatcher", UIParent) + local watcher = getglobal("NanamiAFKWatcher") or CreateFrame("Frame", "NanamiAFKWatcher", UIParent) watcher._checkTimer = 0 watcher._lastCursorX = 0 watcher._lastCursorY = 0 + watcher:Show() 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 @@ -1494,7 +1548,6 @@ function AFK:Initialize() end end) - -- Server AFK message as secondary instant trigger SFrames:RegisterEvent("CHAT_MSG_SYSTEM", function() AFK:OnSystemMessage(arg1) end) @@ -1526,4 +1579,8 @@ function AFK:Initialize() end end end) + + SFrames:RegisterEvent("PLAYER_LEAVING_WORLD", function() + AFK:Cleanup() + end) end diff --git a/CharacterPanel.lua b/CharacterPanel.lua index c1440c7..0869657 100644 --- a/CharacterPanel.lua +++ b/CharacterPanel.lua @@ -3109,73 +3109,136 @@ function CP:BuildSkillsPage() 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 = {} +do + local TRADE_HEADERS = { ["Trade Skills"] = true, ["专业技能"] = true, ["Professions"] = true, + ["商业技能"] = true, ["专业"] = true } + local pendingAbandonIndex - 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 + StaticPopupDialogs = StaticPopupDialogs or {} + StaticPopupDialogs["NANAMI_ABANDON_SKILL"] = { + text = "确定要遗弃 %s 吗?\n该操作不可撤销!", + button1 = "确定", + button2 = "取消", + OnAccept = function() + if pendingAbandonIndex and AbandonSkill then + AbandonSkill(pendingAbandonIndex) + pendingAbandonIndex = nil 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, + OnCancel = function() pendingAbandonIndex = nil end, + timeout = 0, whileDead = true, hideOnEscape = true, + } + + 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 + local currentHeader = "" + local delBtnSize = 14 + + for i = 1, numSkills do + local sn, isH, isE, sr, nt, sm, smr, isAbandonable + if GetSkillLineInfo then sn, isH, isE, sr, nt, sm, smr, isAbandonable = GetSkillLineInfo(i) end + if not sn then break end + + if isH then + currentHeader = sn or "" + 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 canAbandon = TRADE_HEADERS[currentHeader] + local rightPad = canAbandon and (delBtnSize + 6) or 0 + + 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", -rightPad, -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", -rightPad, 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 - rightPad) * Clamp((sr or 0) / smr, 0, 1), 1)) + end + + if canAbandon then + local db = CreateFrame("Button", nil, sf) + db:SetWidth(delBtnSize) + db:SetHeight(delBtnSize) + db:SetPoint("RIGHT", sf, "RIGHT", 0, 0) + db:SetFrameLevel(sf:GetFrameLevel() + 2) + SetPixelBackdrop(db, { 0.25, 0.08, 0.08, 0.7 }, { 0.5, 0.15, 0.15, 0.6 }) + local ico = SFrames:CreateIcon(db, "close", 8) + ico:SetDrawLayer("OVERLAY") + ico:SetPoint("CENTER", db, "CENTER", 0, 0) + ico:SetVertexColor(0.9, 0.4, 0.4) + db.skillIndex = i + db.skillName = sn + db:SetScript("OnClick", function() + pendingAbandonIndex = this.skillIndex + if StaticPopup_Show then + StaticPopup_Show("NANAMI_ABANDON_SKILL", this.skillName) + end + end) + db:SetScript("OnEnter", function() + this:SetBackdropColor(0.45, 0.1, 0.1, 0.9) + this:SetBackdropBorderColor(0.8, 0.2, 0.2, 0.9) + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:AddLine("遗弃 " .. (this.skillName or ""), 1, 0.4, 0.4) + GameTooltip:AddLine("点击遗弃该技能", 0.7, 0.7, 0.7) + GameTooltip:Show() + end) + db:SetScript("OnLeave", function() + this:SetBackdropColor(0.25, 0.08, 0.08, 0.7) + this:SetBackdropBorderColor(0.5, 0.15, 0.15, 0.6) + GameTooltip:Hide() + end) + end + + table.insert(page.skillRows, { frame = sf }) + y = y - rowH + end + end + page.scrollArea:SetContentHeight(math.abs(y) + 16) end - page.scrollArea:SetContentHeight(math.abs(y) + 16) end -------------------------------------------------------------------------------- diff --git a/Chat.lua b/Chat.lua index 5ceb128..83d3445 100644 --- a/Chat.lua +++ b/Chat.lua @@ -74,8 +74,8 @@ local DEFAULT_FILTERS = { raid = true, whisper = true, system = true, - loot = false, - money = false, + loot = true, + money = true, } local AUTO_TRANSLATE_TARGET_LANG = "zh" @@ -737,6 +737,9 @@ local function GetChannelNameFromChatLine(text) local _, _, label = string.find(text, "|Hchannel:[^|]+|h%[([^%]]+)%]|h") if not label then + if string.byte(text, 1) ~= 124 and string.byte(text, 1) ~= 91 then + return nil + end local raw = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") raw = string.gsub(raw, "|r", "") raw = string.gsub(raw, "^%s+", "") @@ -1296,7 +1299,7 @@ end local function BuildDefaultTab(id, name) return { id = id, - name = name or ("Tab" .. tostring(id)), + name = name or ("标签" .. tostring(id)), filters = CopyTable(DEFAULT_FILTERS), channelFilters = {}, translateFilters = BuildDefaultTranslateFilters(), @@ -1340,11 +1343,11 @@ local function SanitizeTab(tab, fallbackId, fallbackName) 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)) + tab.name = fallbackName or ("标签" .. tostring(tab.id)) else tab.name = Trim(tab.name) if tab.name == "" then - tab.name = fallbackName or ("Tab" .. tostring(tab.id)) + tab.name = fallbackName or ("标签" .. tostring(tab.id)) end end @@ -1570,7 +1573,7 @@ local function EnsureDB() local maxId = 0 for i = 1, table.getn(db.tabs) do - db.tabs[i] = SanitizeTab(db.tabs[i], i, "Tab" .. tostring(i)) + db.tabs[i] = SanitizeTab(db.tabs[i], i, "标签" .. tostring(i)) if IsCombatTab(db.tabs[i]) then db.tabs[i].kind = "combat" end @@ -1670,6 +1673,26 @@ local function FocusPopupEdit(popup) if eb.HighlightText then eb:HighlightText() end end +local function SkinPopupEditBox(popup) + local eb = GetPopupEditBox(popup) + if not eb or eb._sfSkinned then return end + eb._sfSkinned = true + local regions = { eb:GetRegions() } + for _, r in ipairs(regions) do + if r and r:GetObjectType() == "Texture" then + r:SetAlpha(0) + end + end + eb: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 }, + }) + eb:SetBackdropColor(0.08, 0.06, 0.1, 0.95) + eb:SetBackdropBorderColor(0.5, 0.4, 0.55, 0.8) +end + local function ResolvePopupFrame(whichKey, dialog) if dialog and dialog.GetParent then local parent = dialog:GetParent() @@ -1740,6 +1763,7 @@ local function EnsurePopupDialogs() OnShow = function(dialog) local popup = ResolvePopupFrame("SFRAMES_CHAT_NEW_TAB", dialog) if not popup then return end + SkinPopupEditBox(popup) local suggested = "Tab" if SFrames and SFrames.Chat and SFrames.Chat.GetNextTabName then suggested = SFrames.Chat:GetNextTabName() @@ -1781,6 +1805,7 @@ local function EnsurePopupDialogs() OnShow = function(dialog, data) local popup = ResolvePopupFrame("SFRAMES_CHAT_RENAME_TAB", dialog) if not popup then return end + SkinPopupEditBox(popup) 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 @@ -1895,7 +1920,7 @@ end function SFrames.Chat:GetNextTabName() local db = EnsureDB() local id = db.nextTabId or (table.getn(db.tabs) + 1) - return "Tab" .. tostring(id) + return "标签" .. tostring(id) end function SFrames.Chat:IsTabProtected(index) @@ -2275,7 +2300,7 @@ function SFrames.Chat:ResetPosition() if not SFramesDB.Positions then SFramesDB.Positions = {} end SFramesDB.Positions["ChatFrame"] = nil self.frame:ClearAllPoints() - self.frame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 30, 30) + self.frame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0) self:SavePosition() if SFrames and SFrames.Print then SFrames:Print("Chat frame position reset.") @@ -2637,12 +2662,12 @@ function SFrames.Chat:RefreshTranslateConfigFrame() if not self.translateConfigFrame then return end if self.translateCurrentTabText then - self.translateCurrentTabText:SetText("Current Tab: " .. self:GetConfigFrameActiveTabName()) + self.translateCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName()) end if self.translateChannelHint then local channels = self:GetJoinedChannels() - self.translateChannelHint:SetText("Joined Channels: " .. tostring(table.getn(channels))) + self.translateChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels))) end if self.translateConfigControls then @@ -2690,7 +2715,7 @@ function SFrames.Chat:EnsureTranslateConfigFrame() local title = panel:CreateFontString(nil, "OVERLAY") title:SetFont(fontPath, 14, "OUTLINE") title:SetPoint("TOP", panel, "TOP", 0, -12) - title:SetText("Chat AI Translate") + title:SetText("聊天 AI 翻译") title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton") @@ -2701,24 +2726,24 @@ function SFrames.Chat:EnsureTranslateConfigFrame() table.insert(controls, ctrl) end - local tabSection = CreateCfgSection(panel, "Tab", 10, -36, 520, 92, fontPath) + local tabSection = CreateCfgSection(panel, "标签页", 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()) + self.translateCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName()) - CreateCfgButton(tabSection, "Prev", 14, -48, 92, 22, function() + CreateCfgButton(tabSection, "上一个", 14, -48, 92, 22, function() SFrames.Chat:StepTab(-1) SFrames.Chat:RefreshConfigFrame() SFrames.Chat:RefreshTranslateConfigFrame() end) - CreateCfgButton(tabSection, "Next", 112, -48, 92, 22, function() + CreateCfgButton(tabSection, "下一个", 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) + local filterSection = CreateCfgSection(panel, "消息翻译", 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) @@ -2739,13 +2764,13 @@ function SFrames.Chat:EnsureTranslateConfigFrame() )) end - local channelSection = CreateCfgSection(panel, "Channel Translate", 10, -266, 520, 198, fontPath) + local channelSection = CreateCfgSection(panel, "频道翻译", 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:") + self.translateChannelHint:SetText("已加入频道:") local maxChannelChecks = 15 for i = 1, maxChannelChecks do @@ -2813,10 +2838,10 @@ function SFrames.Chat:EnsureTranslateConfigFrame() 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:SetText("仅已启用接收的频道可自动翻译") tip:SetTextColor(0.7, 0.7, 0.74) - local close = CreateCfgButton(panel, "Close", 200, -474, 140, 26, function() + local close = CreateCfgButton(panel, "关闭", 200, -474, 140, 26, function() SFrames.Chat.translateConfigFrame:Hide() end) StyleCfgButton(close) @@ -3961,18 +3986,36 @@ function SFrames.Chat:EnsureConfigFrame() do local hcControls = CreateCfgSection(hcPage, "硬核生存服务器专属", 0, 0, 584, 182, fontPath) + local hcStatusText = hcControls:CreateFontString(nil, "OVERLAY") + hcStatusText:SetFont(fontPath, 10, "OUTLINE") + AddControl(CreateCfgCheck(hcControls, "全局彻底关闭硬核频道接收", 16, -30, function() return EnsureDB().hcGlobalDisable == true end, function(checked) EnsureDB().hcGlobalDisable = (checked == true) end, - function() SFrames.Chat:RefreshConfigFrame() end + function(checked) + SendChatMessage(".hcc", "SAY") + if checked then + hcStatusText:SetText("HC Chat is now |cffff4444OFF|r") + hcStatusText:SetTextColor(1, 0.4, 0.4) + else + hcStatusText:SetText("HC Chat is now |cff44ff44ON|r") + hcStatusText:SetTextColor(0.4, 1, 0.4) + end + SFrames.Chat:RefreshConfigFrame() + end )) + hcStatusText:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 230, -32) + hcStatusText:SetWidth(200) + hcStatusText:SetJustifyH("LEFT") + hcStatusText:SetText("") + 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:SetText("彻底无视HC频道的强制聊天推送。勾选后,所有标签都不会收到硬核频道内容。(即时生效)") hcTip:SetTextColor(0.8, 0.7, 0.7) AddControl(CreateCfgCheck(hcControls, "全局屏蔽玩家死亡/满级信息", 16, -86, @@ -3990,7 +4033,7 @@ function SFrames.Chat:EnsureConfigFrame() 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() return EnsureDB().hcDeathLevelMin or 10 end, function(v) EnsureDB().hcDeathLevelMin = v end, function(v) return (v == 0) and "所有击杀" or (tostring(v) .. " 级及以上") end, function() SFrames.Chat:RefreshConfigFrame() end @@ -4001,12 +4044,6 @@ function SFrames.Chat:EnsureConfigFrame() 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 @@ -4052,7 +4089,6 @@ 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() @@ -4245,7 +4281,7 @@ function SFrames.Chat:CreateContainer() local f = CreateFrame("Frame", "SFramesChatContainer", UIParent) f:SetWidth(DEFAULTS.width) f:SetHeight(DEFAULTS.height) - f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 30, 30) + f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0) f:SetMovable(true) f:EnableMouse(true) f:RegisterForDrag("LeftButton") @@ -4455,7 +4491,42 @@ function SFrames.Chat:CreateContainer() hint:Hide() f.hint = hint - local leftCat = SFrames:CreateIcon(f, "logo", 14) + local titleBtn = CreateFrame("Button", nil, f) + titleBtn:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -2) + titleBtn:SetHeight(20) + titleBtn:SetFrameStrata("HIGH") + titleBtn:SetFrameLevel(f:GetFrameLevel() + 20) + titleBtn:RegisterForClicks("LeftButtonUp") + local titleBtnThrottle = 0 + titleBtn:SetScript("OnUpdate", function() + titleBtnThrottle = titleBtnThrottle + arg1 + if titleBtnThrottle < 0.5 then return end + titleBtnThrottle = 0 + local tw = title:GetStringWidth() or 40 + this:SetWidth(tw + 28) + end) + titleBtn:SetScript("OnClick", function() + if SFrames and SFrames.ConfigUI then + SFrames.ConfigUI:OpenUI() + end + end) + titleBtn:SetScript("OnEnter", function() + title:SetTextColor(1, 0.92, 1) + if f.leftCat then f.leftCat:SetVertexColor(1, 0.92, 1, 1) end + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") + GameTooltip:ClearLines() + GameTooltip:AddLine("Nanami UI 设置", 1, 0.84, 0.94) + GameTooltip:AddLine("点击打开主设置面板", 0.85, 0.85, 0.85) + GameTooltip:Show() + end) + titleBtn:SetScript("OnLeave", function() + title:SetTextColor(1, 0.82, 0.93) + if f.leftCat then f.leftCat:SetVertexColor(1, 0.82, 0.9, 0.8) end + GameTooltip:Hide() + end) + f.titleBtn = titleBtn + + local leftCat = SFrames:CreateIcon(titleBtn, "logo", 14) leftCat:SetDrawLayer("OVERLAY") leftCat:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -5) leftCat:SetVertexColor(1, 0.82, 0.9, 0.8) @@ -4476,7 +4547,7 @@ function SFrames.Chat:CreateContainer() local tabBar = CreateFrame("Frame", nil, f) tabBar:SetPoint("LEFT", title, "RIGHT", 10, -1) - tabBar:SetPoint("RIGHT", configButton, "LEFT", -8, -1) + tabBar:SetPoint("RIGHT", configButton, "LEFT", -28, -1) tabBar:SetHeight(18) f.tabBar = tabBar @@ -4571,7 +4642,7 @@ function SFrames.Chat:CreateContainer() hiddenConfigButton:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 14, 132) hiddenConfigButton:SetFrameStrata("DIALOG") hiddenConfigButton:SetFrameLevel(220) - hiddenConfigButton:SetText("Chat Set") + hiddenConfigButton:SetText("聊天设置") hiddenConfigButton:SetScript("OnClick", function() if SFrames and SFrames.Chat then SFrames.Chat:ToggleConfigFrame() @@ -4580,9 +4651,9 @@ function SFrames.Chat:CreateContainer() 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:AddLine("聊天设置", 1, 0.84, 0.94) + GameTooltip:AddLine("聊天界面隐藏时显示此按钮", 0.86, 0.86, 0.86) + GameTooltip:AddLine("点击打开 Nanami 聊天配置", 0.86, 0.86, 0.86) GameTooltip:Show() end) hiddenConfigButton:SetScript("OnLeave", function() @@ -4599,7 +4670,7 @@ function SFrames.Chat:CreateContainer() f:ClearAllPoints() f:SetPoint(saved.point, UIParent, saved.relativePoint, saved.xOfs, saved.yOfs) else - f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 30, 30) + f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0) end f:SetWidth(Clamp(db.width, 320, 900)) f:SetHeight(Clamp(db.height, 120, 460)) @@ -5507,8 +5578,8 @@ function SFrames.Chat:RefreshTabButtons() if not self.addTabButton then local addBtn = CreateFrame("Button", nil, self.frame.tabBar) - addBtn:SetHeight(18) - addBtn:SetWidth(20) + addBtn:SetHeight(20) + addBtn:SetWidth(28) addBtn:RegisterForClicks("LeftButtonUp") EnsureButtonSkin(addBtn) addBtn.sfText:SetText("+") @@ -5535,8 +5606,8 @@ function SFrames.Chat:RefreshTabButtons() 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:AddLine("新建标签", 1, 0.84, 0.94) + GameTooltip:AddLine("创建聊天标签页", 0.85, 0.85, 0.85) GameTooltip:Show() end) addBtn:SetScript("OnLeave", function() @@ -5551,7 +5622,7 @@ function SFrames.Chat:RefreshTabButtons() end local gap = 3 - local addWidth = 20 + local addWidth = 28 local barWidth = self.frame.tabBar:GetWidth() or 0 if barWidth <= 0 then barWidth = (self.frame:GetWidth() or DEFAULTS.width) - 180 @@ -5579,7 +5650,7 @@ function SFrames.Chat:RefreshTabButtons() 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)) + btn.sfText:SetText(ShortText(tab.name or ("标签" .. tostring(i)), maxChars)) end local idx = i @@ -5601,9 +5672,9 @@ function SFrames.Chat:RefreshTabButtons() 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:AddLine(tab.name or ("标签" .. tostring(idx)), 1, 0.84, 0.94) + GameTooltip:AddLine("左键: 切换标签", 0.82, 0.82, 0.82) + GameTooltip:AddLine("右键: 打开菜单", 1, 0.68, 0.79) GameTooltip:Show() end) btn:SetScript("OnLeave", function() @@ -6174,7 +6245,7 @@ function SFrames.Chat:ApplyConfig() 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:SetPoint("RIGHT", self.frame.configButton, "LEFT", -28, -1) self.frame.tabBar:SetHeight(18) end diff --git a/ClassSkillData.lua b/ClassSkillData.lua index 3c6001f..75b8a92 100644 --- a/ClassSkillData.lua +++ b/ClassSkillData.lua @@ -93,6 +93,7 @@ SFrames.ClassSkillData = { [60] = {"治疗宠物 7级", "奥术射击 8级", "扰乱射击 6级", "冰冻陷阱 3级", "摔绊 3级"}, }, ROGUE = { + [2] = {"潜行"}, [4] = {"背刺", "搜索"}, [6] = {"邪恶攻击 2级", "凿击"}, [8] = {"刺骨 2级", "闪避"}, diff --git a/ConfigUI.lua b/ConfigUI.lua index aa3283a..91298ac 100644 --- a/ConfigUI.lua +++ b/ConfigUI.lua @@ -471,6 +471,7 @@ local function EnsureDB() if SFramesDB.afkOutsideRest == nil then SFramesDB.afkOutsideRest = false end if SFramesDB.trainerReminder == nil then SFramesDB.trainerReminder = true end + if SFramesDB.trainerCache == nil then SFramesDB.trainerCache = {} end if SFramesDB.smoothBars == nil then SFramesDB.smoothBars = true end if SFramesDB.mobRealHealth == nil then SFramesDB.mobRealHealth = true end @@ -3021,6 +3022,23 @@ function SFrames.ConfigUI:BuildPersonalizePage() if SFrames.Player and SFrames.Player.ShowTrainerReminder then local testLevel = UnitLevel("player") if mod(testLevel, 2) ~= 0 then testLevel = testLevel + 1 end + local _, classEn = UnitClass("player") + local cachedSkills = SFramesDB and SFramesDB.trainerCache and SFramesDB.trainerCache[classEn] + local staticSkills = SFrames.ClassSkillData and SFrames.ClassSkillData[classEn] + local function hasData(lv) + if cachedSkills then + if cachedSkills[lv] then return true end + if lv > 1 and cachedSkills[lv - 1] then return true end + end + if staticSkills and staticSkills[lv] then return true end + return false + end + if not hasData(testLevel) then + while testLevel <= 60 and not hasData(testLevel) do + testLevel = testLevel + 2 + end + if testLevel > 60 then testLevel = 60 end + end SFrames.Player:ShowTrainerReminder(testLevel) end end) diff --git a/FlightData.lua b/FlightData.lua index fb8cbe5..290ea12 100644 --- a/FlightData.lua +++ b/FlightData.lua @@ -941,72 +941,232 @@ FTCData = { -- 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 +-- Last sync: 2026-03-16 NanamiLearnedFlights = { - -- 铁炉堡 / 铁炉堡机场 区域 + -- 铁炉堡机场, 丹莫罗 ["铁炉堡机场, 丹莫罗->米奈希尔港,湿地"] = 46, - ["米奈希尔港,湿地->铁炉堡机场, 丹莫罗"] = 43, ["铁炉堡机场, 丹莫罗->丹阿格拉斯, 湿地"] = 25, - ["丹阿格拉斯, 湿地->铁炉堡机场, 丹莫罗"] = 29, ["铁炉堡机场, 丹莫罗->铁炉堡,丹莫罗"] = 80, - ["铁炉堡,丹莫罗->铁炉堡机场, 丹莫罗"] = 80, ["铁炉堡机场, 丹莫罗->南海镇,希尔斯布莱德"] = 138, ["铁炉堡机场, 丹莫罗->暴风城,艾尔文森林"] = 237, ["铁炉堡机场, 丹莫罗->哨兵岭,西部荒野"] = 297, ["铁炉堡机场, 丹莫罗->丹基塔斯,冷酷海岸"] = 193, ["铁炉堡机场, 丹莫罗->塞尔萨玛,洛克莫丹"] = 86, ["铁炉堡机场, 丹莫罗->阿尔萨拉斯"] = 479, + ["铁炉堡机场, 丹莫罗->鹰巢山,辛特兰"] = 203, + ["铁炉堡机场, 丹莫罗->圣光之愿礼拜堂,东瘟疫之地"] = 356, + ["铁炉堡机场, 丹莫罗->湖畔镇,赤脊山"] = 230, + ["铁炉堡机场, 丹莫罗->卡兰之墓,拉匹迪斯之岛"] = 575, + ["铁炉堡机场, 丹莫罗->瑟银哨塔,灼热峡谷"] = 121, + ["铁炉堡机场, 丹莫罗->摩根的岗哨,燃烧平原"] = 203, + ["铁炉堡机场, 丹莫罗->避难谷地,阿拉希高地"] = 143, + ["铁炉堡机场, 丹莫罗->藏宝海湾,荆棘谷"] = 454, + ["铁炉堡机场, 丹莫罗->寒风营地,西瘟疫之地\t"] = 212, + -- 铁炉堡,丹莫罗 + ["铁炉堡,丹莫罗->铁炉堡机场, 丹莫罗"] = 80, ["铁炉堡,丹莫罗->丹阿格拉斯, 湿地"] = 107, ["铁炉堡,丹莫罗->暴风城,艾尔文森林"] = 198, ["铁炉堡,丹莫罗->丹基塔斯,冷酷海岸"] = 202, - -- 暴风城 区域 + ["铁炉堡,丹莫罗->南海镇,希尔斯布莱德"] = 249, + ["铁炉堡,丹莫罗->卡兰之墓,拉匹迪斯之岛"] = 537, + ["铁炉堡,丹莫罗->阿尔萨拉斯"] = 451, + -- 暴风城,艾尔文森林 ["暴风城,艾尔文森林->铁炉堡机场, 丹莫罗"] = 286, ["暴风城,艾尔文森林->铁炉堡,丹莫罗"] = 244, ["暴风城,艾尔文森林->藏宝海湾,荆棘谷"] = 230, ["暴风城,艾尔文森林->摩根的岗哨,燃烧平原"] = 148, ["暴风城,艾尔文森林->丹阿格拉斯, 湿地"] = 309, ["暴风城,艾尔文森林->丹基塔斯,冷酷海岸"] = 406, - -- 丹阿格拉斯 区域 + ["暴风城,艾尔文森林->南海镇,希尔斯布莱德"] = 418, + ["暴风城,艾尔文森林->湖畔镇,赤脊山"] = 106, + ["暴风城,艾尔文森林->米奈希尔港,湿地"] = 323, + ["暴风城,艾尔文森林->夜色镇,暮色森林"] = 109, + ["暴风城,艾尔文森林->哨兵岭,西部荒野"] = 73, + ["暴风城,艾尔文森林->卡兰之墓,拉匹迪斯之岛"] = 351, + ["暴风城,艾尔文森林->阿尔萨拉斯"] = 654, + -- 丹阿格拉斯, 湿地 ["丹阿格拉斯, 湿地->米奈希尔港,湿地"] = 20, + ["丹阿格拉斯, 湿地->铁炉堡机场, 丹莫罗"] = 29, ["丹阿格拉斯, 湿地->铁炉堡,丹莫罗"] = 110, ["丹阿格拉斯, 湿地->藏宝海湾,荆棘谷"] = 484, ["丹阿格拉斯, 湿地->卡兰之墓,拉匹迪斯之岛"] = 605, ["丹阿格拉斯, 湿地->塞尔萨玛,洛克莫丹"] = 150, ["丹阿格拉斯, 湿地->暴风城,艾尔文森林"] = 266, - -- 米奈希尔港 区域 + ["丹阿格拉斯, 湿地->南海镇,希尔斯布莱德"] = 113, + ["丹阿格拉斯, 湿地->丹基塔斯,冷酷海岸"] = 257, + ["丹阿格拉斯, 湿地->避难谷地,阿拉希高地"] = 118, + ["丹阿格拉斯, 湿地->哨兵岭,西部荒野"] = 327, + -- 米奈希尔港,湿地 ["米奈希尔港,湿地->丹阿格拉斯, 湿地"] = 28, - -- 南海镇 区域 + ["米奈希尔港,湿地->铁炉堡机场, 丹莫罗"] = 43, + ["米奈希尔港,湿地->卡兰之墓,拉匹迪斯之岛"] = 584, + ["米奈希尔港,湿地->丹基塔斯,冷酷海岸"] = 260, + -- 南海镇,希尔斯布莱德 ["南海镇,希尔斯布莱德->丹阿格拉斯, 湿地"] = 129, - -- 摩根的岗哨 / 瑟银哨塔 区域 - ["摩根的岗哨,燃烧平原->瑟银哨塔,灼热峡谷"] = 97, - ["瑟银哨塔,灼热峡谷->摩根的岗哨,燃烧平原"] = 90, - ["瑟银哨塔,灼热峡谷->藏宝海湾,荆棘谷"] = 329, - -- 藏宝海湾 区域 - ["藏宝海湾,荆棘谷->铁炉堡机场, 丹莫罗"] = 460, - ["藏宝海湾,荆棘谷->卡兰之墓,拉匹迪斯之岛"] = 121, - ["藏宝海湾,荆棘谷->丹阿格拉斯, 湿地"] = 483, - ["卡兰之墓,拉匹迪斯之岛->藏宝海湾,荆棘谷"] = 121, - -- 加基森 / 塞拉摩 区域 - ["加基森,塔纳利斯->塞拉摩,尘泥沼泽"] = 144, - ["塞拉摩,尘泥沼泽->加基森,塔纳利斯"] = 147, - -- 安伯郡 / 哨兵岭 区域 - ["安伯郡,北风领->暴风城,艾尔文森林"] = 107, - ["哨兵岭,西部荒野->丹阿格拉斯, 湿地"] = 377, - ["哨兵岭,西部荒野->铁炉堡机场, 丹莫罗"] = 354, - -- 丹基塔斯 (Turtle WoW 冷酷海岸) + ["南海镇,希尔斯布莱德->铁炉堡机场, 丹莫罗"] = 144, + ["南海镇,希尔斯布莱德->卡兰之墓,拉匹迪斯之岛"] = 683, + ["南海镇,希尔斯布莱德->阿尔萨拉斯"] = 343, + ["南海镇,希尔斯布莱德->丹基塔斯,冷酷海岸"] = 336, + -- 丹基塔斯,冷酷海岸 ["丹基塔斯,冷酷海岸->铁炉堡,丹莫罗"] = 210, ["丹基塔斯,冷酷海岸->铁炉堡机场, 丹莫罗"] = 204, ["丹基塔斯,冷酷海岸->暴风城,艾尔文森林"] = 371, ["丹基塔斯,冷酷海岸->哨兵岭,西部荒野"] = 431, ["丹基塔斯,冷酷海岸->塞尔萨玛,洛克莫丹"] = 107, - -- 塞尔萨玛 区域 + ["丹基塔斯,冷酷海岸->丹阿格拉斯, 湿地"] = 263, + ["丹基塔斯,冷酷海岸->米奈希尔港,湿地"] = 251, + ["丹基塔斯,冷酷海岸->摩根的岗哨,燃烧平原"] = 333, + ["丹基塔斯,冷酷海岸->南海镇,希尔斯布莱德"] = 343, + ["丹基塔斯,冷酷海岸->鹰巢山,辛特兰"] = 329, + ["丹基塔斯,冷酷海岸->卡兰之墓,拉匹迪斯之岛"] = 709, + ["丹基塔斯,冷酷海岸->藏宝海湾,荆棘谷"] = 588, + ["丹基塔斯,冷酷海岸->瑟银哨塔,灼热峡谷"] = 252, + ["丹基塔斯,冷酷海岸->避难谷地,阿拉希高地"] = 261, + -- 塞尔萨玛,洛克莫丹 ["塞尔萨玛,洛克莫丹->铁炉堡机场, 丹莫罗"] = 96, - -- 阿尔萨拉斯 / 圣光之愿 / 鹰巢山 区域 + ["塞尔萨玛,洛克莫丹->丹基塔斯,冷酷海岸"] = 107, + -- 哨兵岭,西部荒野 + ["哨兵岭,西部荒野->丹阿格拉斯, 湿地"] = 377, + ["哨兵岭,西部荒野->铁炉堡机场, 丹莫罗"] = 354, + ["哨兵岭,西部荒野->暴风城,艾尔文森林"] = 81, + ["哨兵岭,西部荒野->南海镇,希尔斯布莱德"] = 486, + ["哨兵岭,西部荒野->丹基塔斯,冷酷海岸"] = 474, + ["哨兵岭,西部荒野->卡兰之墓,拉匹迪斯之岛"] = 296, + -- 安伯郡,北风领 + ["安伯郡,北风领->暴风城,艾尔文森林"] = 107, + -- 湖畔镇,赤脊山 + ["湖畔镇,赤脊山->铁炉堡机场, 丹莫罗"] = 262, + ["湖畔镇,赤脊山->卡兰之墓,拉匹迪斯之岛"] = 336, + ["湖畔镇,赤脊山->守望堡,诅咒之地"] = 137, + -- 夜色镇,暮色森林 + ["夜色镇,暮色森林->湖畔镇,赤脊山"] = 56, + ["夜色镇,暮色森林->哨兵岭,西部荒野"] = 87, + ["夜色镇,暮色森林->米奈希尔港,湿地"] = 405, + ["夜色镇,暮色森林->卡兰之墓,拉匹迪斯之岛"] = 283, + -- 避难谷地,阿拉希高地 + ["避难谷地,阿拉希高地->丹阿格拉斯, 湿地"] = 146, + ["避难谷地,阿拉希高地->丹基塔斯,冷酷海岸"] = 267, + ["避难谷地,阿拉希高地->铁炉堡机场, 丹莫罗"] = 161, + -- 守望堡,诅咒之地 + ["守望堡,诅咒之地->铁炉堡机场, 丹莫罗"] = 452, + -- 摩根的岗哨,燃烧平原 + ["摩根的岗哨,燃烧平原->瑟银哨塔,灼热峡谷"] = 97, + ["摩根的岗哨,燃烧平原->铁炉堡机场, 丹莫罗"] = 218, + ["摩根的岗哨,燃烧平原->卡兰之墓,拉匹迪斯之岛"] = 393, + -- 瑟银哨塔,灼热峡谷 + ["瑟银哨塔,灼热峡谷->摩根的岗哨,燃烧平原"] = 90, + ["瑟银哨塔,灼热峡谷->藏宝海湾,荆棘谷"] = 329, + ["瑟银哨塔,灼热峡谷->湖畔镇,赤脊山"] = 117, + ["瑟银哨塔,灼热峡谷->铁炉堡机场, 丹莫罗"] = 132, + ["瑟银哨塔,灼热峡谷->莫尔奥格避难所,吉利吉姆之岛"] = 520, + ["瑟银哨塔,灼热峡谷->碎风哨站,巴洛"] = 551, + -- 阿尔萨拉斯 ["阿尔萨拉斯->鹰巢山,辛特兰"] = 276, ["阿尔萨拉斯->圣光之愿礼拜堂,东瘟疫之地"] = 125, + ["阿尔萨拉斯->暴风城,艾尔文森林"] = 635, + ["阿尔萨拉斯->铁炉堡,丹莫罗"] = 472, + ["阿尔萨拉斯->寒风营地,西瘟疫之地\t"] = 263, + -- 圣光之愿礼拜堂,东瘟疫之地 ["圣光之愿礼拜堂,东瘟疫之地->阿尔萨拉斯"] = 124, + ["圣光之愿礼拜堂,东瘟疫之地->哨兵岭,西部荒野"] = 571, + -- 鹰巢山,辛特兰 ["鹰巢山,辛特兰->丹阿格拉斯, 湿地"] = 190, - -- 月光林地 / 石爪峰 区域 + ["鹰巢山,辛特兰->丹基塔斯,冷酷海岸"] = 338, + ["鹰巢山,辛特兰->铁炉堡机场, 丹莫罗"] = 205, + -- 寒风营地,西瘟疫之地 + ["寒风营地,西瘟疫之地\t->丹阿格拉斯, 湿地"] = 199, + ["寒风营地,西瘟疫之地\t->铁炉堡机场, 丹莫罗"] = 215, + -- 藏宝海湾,荆棘谷 + ["藏宝海湾,荆棘谷->铁炉堡机场, 丹莫罗"] = 460, + ["藏宝海湾,荆棘谷->卡兰之墓,拉匹迪斯之岛"] = 121, + ["藏宝海湾,荆棘谷->丹阿格拉斯, 湿地"] = 483, + ["藏宝海湾,荆棘谷->丹基塔斯,冷酷海岸"] = 580, + ["藏宝海湾,荆棘谷->莫尔奥格避难所,吉利吉姆之岛"] = 61, + -- 卡兰之墓,拉匹迪斯之岛 + ["卡兰之墓,拉匹迪斯之岛->藏宝海湾,荆棘谷"] = 121, + ["卡兰之墓,拉匹迪斯之岛->丹基塔斯,冷酷海岸"] = 702, + ["卡兰之墓,拉匹迪斯之岛->暴风城,艾尔文森林"] = 328, + ["卡兰之墓,拉匹迪斯之岛->夜色镇,暮色森林"] = 286, + ["卡兰之墓,拉匹迪斯之岛->塞尔萨玛,洛克莫丹"] = 595, + ["卡兰之墓,拉匹迪斯之岛->湖畔镇,赤脊山"] = 338, + ["卡兰之墓,拉匹迪斯之岛->米奈希尔港,湿地"] = 619, + ["卡兰之墓,拉匹迪斯之岛->哨兵岭,西部荒野"] = 293, + ["卡兰之墓,拉匹迪斯之岛->丹阿格拉斯, 湿地"] = 605, + ["卡兰之墓,拉匹迪斯之岛->铁炉堡,丹莫罗"] = 540, + ["卡兰之墓,拉匹迪斯之岛->铁炉堡机场, 丹莫罗"] = 582, + -- 碎刃哨岗,冷酷海岸 (部落) + ["碎刃哨岗,冷酷海岸->塔伦米尔,希尔斯布莱德"] = 298, + ["碎刃哨岗,冷酷海岸->恶齿村,辛特兰"] = 273, + ["碎刃哨岗,冷酷海岸->卡加斯,荒芜之地"] = 432, + ["碎刃哨岗,冷酷海岸->格罗姆高,荆棘谷"] = 726, + ["碎刃哨岗,冷酷海岸->寂静守卫教堂, 吉尔尼斯"] = 374, + ["碎刃哨岗,冷酷海岸->瑟伯切尔,银松森林"] = 390, + ["碎刃哨岗,冷酷海岸->幽暗城,提瑞斯法林地"] = 430, + ["碎刃哨岗,冷酷海岸->落锤镇,阿拉希高地"] = 187, + -- 寂静守卫教堂, 吉尔尼斯 (部落) + ["寂静守卫教堂, 吉尔尼斯->瑟伯切尔,银松森林"] = 171, + ["寂静守卫教堂, 吉尔尼斯->恶齿村,辛特兰"] = 263, + ["寂静守卫教堂, 吉尔尼斯->碎刃哨岗,冷酷海岸"] = 374, + ["寂静守卫教堂, 吉尔尼斯->格罗姆高,荆棘谷"] = 728, + ["寂静守卫教堂, 吉尔尼斯->幽暗城,提瑞斯法林地"] = 211, + ["寂静守卫教堂, 吉尔尼斯->卡加斯,荒芜之地"] = 434, + ["寂静守卫教堂, 吉尔尼斯->落锤镇,阿拉希高地"] = 190, + ["寂静守卫教堂, 吉尔尼斯->塔伦米尔,希尔斯布莱德"] = 78, + -- 莫尔奥格避难所,吉利吉姆之岛 (部落) + ["莫尔奥格避难所,吉利吉姆之岛->藏宝海湾,荆棘谷"] = 61, + ["莫尔奥格避难所,吉利吉姆之岛->斯通纳德,悲伤沼泽"] = 313, + ["莫尔奥格避难所,吉利吉姆之岛->落锤镇,阿拉希高地"] = 674, + ["莫尔奥格避难所,吉利吉姆之岛->格罗姆高,荆棘谷"] = 158, + ["莫尔奥格避难所,吉利吉姆之岛->烈焰峰,燃烧平原"] = 499, + ["莫尔奥格避难所,吉利吉姆之岛->卡加斯,荒芜之地"] = 454, + ["莫尔奥格避难所,吉利吉姆之岛->幽暗城,提瑞斯法林地"] = 897, + ["莫尔奥格避难所,吉利吉姆之岛->瑟银哨塔,灼热峡谷"] = 507, + ["莫尔奥格避难所,吉利吉姆之岛->碎风哨站,巴洛"] = 349, + -- 格罗姆高,荆棘谷 (部落) + ["格罗姆高,荆棘谷->碎刃哨岗,冷酷海岸"] = 711, + ["格罗姆高,荆棘谷->莫尔奥格避难所,吉利吉姆之岛"] = 137, + ["格罗姆高,荆棘谷->斯通纳德,悲伤沼泽"] = 194, + ["格罗姆高,荆棘谷->寂静守卫教堂, 吉尔尼斯"] = 714, + ["格罗姆高,荆棘谷->碎风哨站,巴洛"] = 191, + ["格罗姆高,荆棘谷->瑟银哨塔,灼热峡谷"] = 361, + -- 碎风哨站,巴洛 (部落) + ["碎风哨站,巴洛->藏宝海湾,荆棘谷"] = 267, + ["碎风哨站,巴洛->格罗姆高,荆棘谷"] = 191, + ["碎风哨站,巴洛->莫尔奥格避难所,吉利吉姆之岛"] = 329, + ["碎风哨站,巴洛->卡加斯,荒芜之地"] = 500, + ["碎风哨站,巴洛->斯通纳德,悲伤沼泽"] = 385, + -- 卡加斯,荒芜之地 (部落) + ["卡加斯,荒芜之地->碎刃哨岗,冷酷海岸"] = 431, + ["卡加斯,荒芜之地->莫尔奥格避难所,吉利吉姆之岛"] = 455, + ["卡加斯,荒芜之地->碎风哨站,巴洛"] = 486, + ["卡加斯,荒芜之地->寂静守卫教堂, 吉尔尼斯"] = 433, + -- 瑟伯切尔,银松森林 (部落) + ["瑟伯切尔,银松森林->寂静守卫教堂, 吉尔尼斯"] = 166, + ["瑟伯切尔,银松森林->碎刃哨岗,冷酷海岸"] = 383, + -- 幽暗城,提瑞斯法林地 (部落) + ["幽暗城,提瑞斯法林地->寂静守卫教堂, 吉尔尼斯"] = 209, + ["幽暗城,提瑞斯法林地->碎刃哨岗,冷酷海岸"] = 428, + ["幽暗城,提瑞斯法林地->莫尔奥格避难所,吉利吉姆之岛"] = 914, + -- 落锤镇,阿拉希高地 (部落) + ["落锤镇,阿拉希高地->寂静守卫教堂, 吉尔尼斯"] = 186, + ["落锤镇,阿拉希高地->莫尔奥格避难所,吉利吉姆之岛"] = 698, + ["落锤镇,阿拉希高地->碎刃哨岗,冷酷海岸"] = 187, + -- 斯通纳德,悲伤沼泽 (部落) + ["斯通纳德,悲伤沼泽->莫尔奥格避难所,吉利吉姆之岛"] = 307, + ["斯通纳德,悲伤沼泽->碎风哨站,巴洛"] = 369, + -- 塔伦米尔,希尔斯布莱德 (部落) + ["塔伦米尔,希尔斯布莱德->恶齿村,辛特兰"] = 185, + ["塔伦米尔,希尔斯布莱德->寂静守卫教堂, 吉尔尼斯"] = 78, + -- 恶齿村,辛特兰 (部落) + ["恶齿村,辛特兰->寂静守卫教堂, 吉尔尼斯"] = 227, + ["恶齿村,辛特兰->碎刃哨岗,冷酷海岸"] = 270, + ["恶齿村,辛特兰->莫尔奥格避难所,吉利吉姆之岛"] = 767, + -- 烈焰峰,燃烧平原 (部落) + ["烈焰峰,燃烧平原->莫尔奥格避难所,吉利吉姆之岛"] = 507, + -- 加基森 / 塞拉摩 + ["加基森,塔纳利斯->塞拉摩,尘泥沼泽"] = 144, + ["塞拉摩,尘泥沼泽->加基森,塔纳利斯"] = 147, + -- 卡利姆多 ["月光林地->石爪峰,石爪山"] = 304, ["石爪峰,石爪山->塔伦迪斯营地,艾萨拉"] = 285, } \ No newline at end of file diff --git a/GearScore.lua b/GearScore.lua index b9ea3e7..9037ea2 100644 --- a/GearScore.lua +++ b/GearScore.lua @@ -24,6 +24,7 @@ local BUDGET_COST = { HEALTHREG = 2.0, MANAREG = 2.0, HEALTH = 0.07, MANA = 0.07, WEAPONDPS = 3.0, + WEAPONSPEED = 1.5, } -------------------------------------------------------------------------------- @@ -31,175 +32,204 @@ local BUDGET_COST = { -------------------------------------------------------------------------------- local WEIGHTS = { + -- ================================================================ + -- Pawn-style normalization: primary stat = 1.0 + -- TOHIT/CRIT per 1%; PDF: 1%hit≈18AP, 1%crit≈25AP, 1%spellhit≈14SP + -- ================================================================ 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 } }, + w = { STR=1.0, AGI=0.7, STA=0.5, TOHIT=9, CRIT=12, + ATTACKPOWER=0.5, HEALTH=0.05, WEAPONDPS=5, BASEARMOR=0.005 } }, { 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 } }, + w = { STR=1.0, AGI=0.6, STA=0.5, TOHIT=10, CRIT=11, + ATTACKPOWER=0.5, HEALTH=0.05, WEAPONDPS=5, BASEARMOR=0.005 } }, { 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 } }, + w = { STA=1.0, STR=0.5, AGI=0.7, TOHIT=5, CRIT=3, ATTACKPOWER=0.2, + DEFENSE=0.8, DODGE=8, PARRY=7, BLOCK=6, BLOCKVALUE=0.35, + ARMOR=0.02, HEALTH=0.1, HEALTHREG=1.0, WEAPONDPS=3, BASEARMOR=0.03 } }, }, -- 硬核物理(战士型): 耐 > 力 > 敏 > 攻强 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 } }, + w = { STA=1.5, STR=1.0, AGI=0.8, ATTACKPOWER=0.5, + TOHIT=3, CRIT=3, DEFENSE=0.5, DODGE=5, PARRY=3, BLOCK=3, BLOCKVALUE=0.3, + ARMOR=0.05, HEALTH=0.1, HEALTHREG=1.5, WEAPONDPS=3, BASEARMOR=0.02 } }, }, 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 } }, + w = { INT=1.0, SPI=0.3, STA=0.5, HEAL=0.55, DMG=0.1, + SPELLCRIT=5, MANAREG=1.3, MANA=0.01, 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 } }, + w = { STA=1.0, STR=0.5, AGI=0.5, INT=0.3, TOHIT=5, CRIT=3, + ATTACKPOWER=0.2, DMG=0.4, + DEFENSE=0.7, DODGE=7, PARRY=6, BLOCK=6, BLOCKVALUE=0.15, + ARMOR=0.02, HEALTH=0.1, MANAREG=1.0, WEAPONDPS=2, BASEARMOR=0.03 } }, { 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 } }, + w = { STR=1.0, AGI=0.6, STA=0.5, INT=0.3, + TOHIT=8, CRIT=10, SPELLCRIT=5, + ATTACKPOWER=0.5, DMG=0.3, HEAL=0.05, WEAPONDPS=5, BASEARMOR=0.005 } }, }, -- 硬核圣骑士: 耐 > 力 > 智 = 敏 = 精 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 } }, + w = { STA=1.5, STR=1.0, INT=0.8, AGI=0.8, SPI=0.8, + TOHIT=3, CRIT=3, ATTACKPOWER=0.3, HEAL=0.4, DMG=0.2, + DEFENSE=0.5, DODGE=3, ARMOR=0.04, HEALTH=0.1, + HEALTHREG=1.0, MANAREG=1.0, WEAPONDPS=2, BASEARMOR=0.02 } }, }, 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 } }, + w = { AGI=1.0, STR=0.05, STA=0.5, INT=0.8, + TOHIT=10, CRIT=10, RANGEDCRIT=10, + ATTACKPOWER=0.4, RANGEDATTACKPOWER=0.5, MANAREG=2.0, + WEAPONDPS=4, 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 } }, + w = { AGI=1.0, STR=0.05, STA=0.5, INT=0.9, + TOHIT=10, CRIT=10, RANGEDCRIT=10, + ATTACKPOWER=0.4, RANGEDATTACKPOWER=0.5, MANAREG=2.0, + WEAPONDPS=4, 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 } }, + w = { AGI=1.0, STR=0.4, STA=0.5, INT=0.3, SPI=0.3, + TOHIT=8, CRIT=8, RANGEDCRIT=8, + ATTACKPOWER=0.5, RANGEDATTACKPOWER=0.5, MANAREG=1.0, + WEAPONDPS=3, BASEARMOR=0.005 } }, }, - -- 硬核猎人: 耐 > 敏 > 智 > 力 + -- 硬核猎人: 耐 > 敏 > 智 > 力 (精神配合灵魂链接有价值) 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 } }, + w = { STA=1.5, AGI=1.0, INT=0.5, STR=0.3, SPI=0.8, + TOHIT=3, CRIT=3, RANGEDCRIT=3, + ATTACKPOWER=0.2, RANGEDATTACKPOWER=0.3, + ARMOR=0.04, HEALTH=0.1, HEALTHREG=1.5, WEAPONDPS=2, BASEARMOR=0.01 } }, }, 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 } }, + w = { AGI=1.0, STR=0.5, STA=0.5, TOHIT=10, CRIT=12, + ATTACKPOWER=0.45, WEAPONDPS=5, BASEARMOR=0.005 } }, { 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 } }, + w = { AGI=1.0, STR=0.5, STA=0.5, TOHIT=10, CRIT=11, + ATTACKPOWER=0.45, WEAPONDPS=6, BASEARMOR=0.005 } }, { 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 } }, + w = { AGI=1.0, STR=0.5, STA=0.5, TOHIT=8, CRIT=10, + ATTACKPOWER=0.45, DODGE=3, WEAPONDPS=4, BASEARMOR=0.005 } }, }, -- 硬核潜行者: 耐 > 敏 > 力 > 攻强 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 } }, + w = { STA=1.5, AGI=1.2, STR=0.8, ATTACKPOWER=0.5, + TOHIT=3, CRIT=3, DODGE=3, ARMOR=0.04, + HEALTH=0.1, HEALTHREG=1.5, WEAPONDPS=3, BASEARMOR=0.01 } }, }, 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 } }, + w = { INT=1.0, SPI=0.5, STA=0.5, HEAL=0.7, DMG=0.1, + SPELLCRIT=4, MANAREG=1.2, MANA=0.01, + WEAPONDPS=2, WEAPONSPEED=0.5, 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 } }, + w = { INT=1.0, SPI=0.7, STA=0.5, HEAL=0.8, DMG=0.1, + SPELLCRIT=3, MANAREG=1.35, MANA=0.01, + WEAPONDPS=2, WEAPONSPEED=0.5, 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 } }, + w = { DMG=1.0, INT=0.2, SPI=0.2, STA=0.5, + SPELLTOHIT=14, SPELLCRIT=8, MANAREG=1.0, + WEAPONDPS=2.5, WEAPONSPEED=0.6, 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 } }, + w = { STA=1.5, INT=1.0, HEAL=1.0, DMG=0.8, SPI=0.8, + SPELLCRIT=2, SPELLTOHIT=3, MANAREG=1.5, + WEAPONDPS=1.5, WEAPONSPEED=0.3, + ARMOR=0.04, HEALTH=0.08, HEALTHREG=0.8, BASEARMOR=0.01 } }, }, 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 } }, + w = { DMG=1.0, INT=0.3, SPI=0.1, STA=0.5, + SPELLTOHIT=10, SPELLCRIT=8, ATTACKPOWER=0.1, + MANAREG=1.1, 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 } }, + w = { STR=1.0, AGI=0.9, STA=0.5, INT=0.3, + TOHIT=10, CRIT=11, ATTACKPOWER=0.5, + DMG=0.3, SPELLCRIT=3, MANAREG=1.0, + WEAPONDPS=5, BASEARMOR=0.005 } }, { 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 } }, + w = { INT=1.0, SPI=0.3, STA=0.5, HEAL=0.9, DMG=0.1, + SPELLCRIT=5, MANAREG=1.7, MANA=0.01, 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 } }, + w = { STA=1.5, STR=0.8, AGI=0.8, INT=0.5, SPI=0.5, + TOHIT=3, CRIT=3, ATTACKPOWER=0.3, HEAL=0.4, DMG=0.3, + MANAREG=1.0, ARMOR=0.04, HEALTH=0.1, HEALTHREG=1.0, + WEAPONDPS=2, BASEARMOR=0.01 } }, }, 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 } }, + w = { DMG=1.0, INT=0.46, SPI=0.6, STA=0.3, + SPELLTOHIT=10, SPELLCRIT=7, MANAREG=1.1, + MANA=0.04, WEAPONDPS=2, WEAPONSPEED=0.5, 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 } }, + w = { DMG=1.0, INT=0.44, SPI=0.07, STA=0.3, + SPELLTOHIT=12, SPELLCRIT=9, MANAREG=1.0, + MANA=0.04, WEAPONDPS=2, WEAPONSPEED=0.5, 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 } }, + w = { DMG=1.0, INT=0.37, SPI=0.06, STA=0.3, + SPELLTOHIT=12, SPELLCRIT=7, MANAREG=0.8, + MANA=0.03, WEAPONDPS=2, WEAPONSPEED=0.5, 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 } }, + w = { STA=1.5, INT=1.0, DMG=1.0, SPI=0.8, + SPELLCRIT=2, SPELLTOHIT=3, MANAREG=1.5, + WEAPONDPS=1.5, WEAPONSPEED=0.3, + ARMOR=0.05, HEALTH=0.08, HEALTHREG=0.8, BASEARMOR=0.01 } }, }, 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 } }, + w = { DMG=1.0, INT=0.4, SPI=0.1, STA=0.5, + SPELLTOHIT=12, SPELLCRIT=4, MANAREG=1.0, + HEALTH=0.05, WEAPONDPS=2, WEAPONSPEED=0.5, 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 } }, + w = { DMG=1.0, INT=0.4, SPI=0.5, STA=0.5, + SPELLTOHIT=12, SPELLCRIT=7, MANAREG=1.0, + WEAPONDPS=2, WEAPONSPEED=0.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 } }, + w = { DMG=1.0, INT=0.34, SPI=0.25, STA=0.5, + SPELLTOHIT=14, SPELLCRIT=9, MANAREG=0.65, + WEAPONDPS=2.5, WEAPONSPEED=0.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 } }, + w = { STA=1.5, INT=1.0, DMG=1.0, SPI=0.5, + SPELLCRIT=2, SPELLTOHIT=3, MANAREG=1.0, + WEAPONDPS=1.5, WEAPONSPEED=0.3, + ARMOR=0.04, HEALTH=0.08, HEALTHREG=0.8, BASEARMOR=0.01 } }, }, 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 } }, + w = { DMG=1.0, INT=0.38, SPI=0.34, STA=0.5, + HEAL=0.1, SPELLTOHIT=12, SPELLCRIT=7, + MANAREG=0.6, MANA=0.03, 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 } }, + w = { STR=1.2, AGI=1.0, STA=0.5, TOHIT=10, CRIT=11, + ATTACKPOWER=0.5, DODGE=1, WEAPONDPS=0.5, BASEARMOR=0.005 } }, { 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 } }, + w = { STA=1.0, AGI=0.5, STR=0.2, TOHIT=3, CRIT=3, ATTACKPOWER=0.35, + DEFENSE=0.5, DODGE=6, ARMOR=0.1, HEALTH=0.08, BASEARMOR=0.04 } }, { 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 } }, + w = { INT=1.0, SPI=0.87, STA=0.5, HEAL=1.2, DMG=0.1, + SPELLCRIT=4, MANAREG=1.7, MANA=0.01, 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 } }, + w = { STA=1.5, AGI=1.0, STR=0.8, INT=0.5, SPI=0.5, + TOHIT=3, CRIT=3, ATTACKPOWER=0.3, HEAL=0.4, DMG=0.3, + DODGE=3, ARMOR=0.04, HEALTH=0.1, HEALTHREG=1.0, + WEAPONDPS=1, BASEARMOR=0.01 } }, }, } @@ -284,36 +314,46 @@ local function AdjustWeightsForLevel(baseWeights, level, isHC) 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 + -- HC: STA always stays high (survival is the whole point) if level <= 20 then - adj.SPI = (adj.SPI or 0) * 1.3 + adj.SPI = math.max((adj.SPI or 0) * 1.3, 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.0 - adj.ARMOR = (adj.ARMOR or 0) + 0.03 - elseif level <= 40 then - adj.SPI = (adj.SPI or 0) * 1.15 + adj.CRIT = (adj.CRIT or 0) * 0.3 + adj.SPELLCRIT = (adj.SPELLCRIT or 0) * 0.3 + adj.HEALTHREG = (adj.HEALTHREG or 0) + 0.8 adj.ARMOR = (adj.ARMOR or 0) + 0.02 + elseif level <= 40 then adj.TOHIT = (adj.TOHIT or 0) * 0.5 adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.5 + adj.ARMOR = (adj.ARMOR or 0) + 0.01 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 + -- PDF: 精神至上 at 1-20; SPI matters for all classes (regen) + adj.SPI = math.max((adj.SPI or 0) * 1.5, 0.3) + adj.STA = (adj.STA or 0) * 0.7 + -- SP/HEAL don't exist on items at this level + adj.DMG = (adj.DMG or 0) * 0.15 + adj.HEAL = (adj.HEAL or 0) * 0.15 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.CRIT = (adj.CRIT or 0) * 0.2 + adj.SPELLCRIT = (adj.SPELLCRIT or 0) * 0.2 + adj.RANGEDCRIT = (adj.RANGEDCRIT or 0) * 0.2 + adj.HEALTHREG = (adj.HEALTHREG or 0) + 0.8 adj.ARMOR = (adj.ARMOR or 0) + 0.02 + elseif level <= 40 then + -- PDF: 40级解锁板甲/锁甲, SP开始出现 + adj.SPI = math.max((adj.SPI or 0) * 1.2, 0.15) + adj.DMG = (adj.DMG or 0) * 0.5 + adj.HEAL = (adj.HEAL or 0) * 0.5 adj.TOHIT = (adj.TOHIT or 0) * 0.6 adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.6 + adj.ARMOR = (adj.ARMOR or 0) + 0.01 else adj.TOHIT = (adj.TOHIT or 0) * 0.85 adj.SPELLTOHIT = (adj.SPELLTOHIT or 0) * 0.85 @@ -517,6 +557,10 @@ local STAT_PATTERNS = { { p = "每秒伤害(%d+%.?%d*)", s = "WEAPONDPS" }, { p = "每秒(%d+%.?%d*)点伤害", s = "WEAPONDPS" }, + -- Weapon attack speed ("速度 1.50" / "Speed 1.50") — stored as raw value + { p = "^速度 (%d+%.?%d*)", s = "WEAPONSPEED" }, + { p = "^Speed (%d+%.?%d*)", s = "WEAPONSPEED" }, + -- Base armor value ("63点护甲" / "123 护甲" / "500 Armor", no + prefix) { p = "^(%d+)点护甲", s = "BASEARMOR" }, { p = "^(%d+) 点护甲", s = "BASEARMOR" }, @@ -544,16 +588,25 @@ local STAT_PATTERNS = { { p = "critical strike with spells by (%d+)%%", s = "SPELLCRIT" }, { p = "法术暴击.-(%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 = "远程.-致命一击.-(%d+)%%", s = "RANGEDCRIT" }, { p = "critical strike by (%d+)%%", s = "CRIT" }, + { p = "致命一击几率.-(%d+)%%", s = "CRIT" }, + { p = "致命一击.-提高(%d+)%%", s = "CRIT" }, { p = "致命一击.-(%d+)%%", s = "CRIT" }, + { p = "暴击几率.-(%d+)%%", s = "CRIT" }, { p = "暴击.-(%d+)%%", s = "CRIT" }, - -- Hit + -- Hit (green equip effects: "使你击中目标的几率提高X%", "chance to hit by X%") { p = "hit with spells by (%d+)%%", s = "SPELLTOHIT" }, + { p = "法术击中.-(%d+)%%", s = "SPELLTOHIT" }, { p = "法术命中.-(%d+)%%", s = "SPELLTOHIT" }, + { p = "用法术击中.-几率.-(%d+)%%", s = "SPELLTOHIT" }, { p = "chance to hit by (%d+)%%", s = "TOHIT" }, + { p = "击中目标.-几率.-(%d+)%%", s = "TOHIT" }, + { p = "击中.-提高(%d+)%%", s = "TOHIT" }, { p = "命中.-(%d+)%%", s = "TOHIT" }, -- Attack Power @@ -591,23 +644,30 @@ local STAT_PATTERNS = { { p = "每5秒恢复(%d+)点生命", s = "HEALTHREG" }, { p = "每5秒回复(%d+)点生命", s = "HEALTHREG" }, - -- Defense + -- Defense (green equip: "提高你的防御技能X点", "+X 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" }, + { p = "防御.-提高(%d+)", s = "DEFENSE" }, - -- Avoidance + -- Avoidance (green equip: "使你的躲闪几率提高X%") { p = "dodge.-by (%d+)%%", s = "DODGE" }, + { p = "躲闪几率.-(%d+)%%", s = "DODGE" }, + { p = "躲闪.-提高(%d+)%%", s = "DODGE" }, { p = "躲闪.-(%d+)%%", s = "DODGE" }, { p = "parry.-by (%d+)%%", s = "PARRY" }, + { p = "招架几率.-(%d+)%%", s = "PARRY" }, + { p = "招架.-提高(%d+)%%", s = "PARRY" }, { p = "招架.-(%d+)%%", s = "PARRY" }, { p = "block attacks.-by (%d+)%%", s = "BLOCK" }, { p = "格挡几率.-(%d+)%%", s = "BLOCK" }, + { p = "格挡.-提高(%d+)%%", s = "BLOCK" }, { p = "格挡率.-(%d+)%%", s = "BLOCK" }, { p = "block value.-by (%d+)", s = "BLOCKVALUE" }, { p = "格挡值.-(%d+)", s = "BLOCKVALUE" }, + { p = "盾牌格挡值.-(%d+)", s = "BLOCKVALUE" }, -- HP/Mana { p = "%+(%d+) Health", s = "HEALTH" }, @@ -725,25 +785,65 @@ end -- ideal_EP = total_budget * best_efficiency_for_spec -------------------------------------------------------------------------------- -local function GetBestEfficiency(weights) - local best = 0 +-- Slot-specific and non-standard stats excluded from reference efficiency +local EFF_EXCLUDE = { WEAPONDPS=true, WEAPONSPEED=true, BASEARMOR=true, HEALTH=true, MANA=true } +-- Stats that only appear on higher-level items +local EFF_LATE_GAME = { DMG=true, HEAL=true, SPELLTOHIT=true, SPELLCRIT=true, + TOHIT=true, CRIT=true, RANGEDCRIT=true } +local EFF_MID_GAME = { DMG=true, HEAL=true } + +local function GetRefEfficiency(weights, level) + local effs = {} 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 + if not EFF_EXCLUDE[stat] then + local cost = BUDGET_COST[stat] + if cost and cost > 0 and w > 0 then + local skip = false + if level and level < 25 and EFF_LATE_GAME[stat] then skip = true end + if level and level >= 25 and level < 40 and EFF_MID_GAME[stat] then skip = true end + if not skip then + table.insert(effs, w / cost) + end + end end end - return best + + table.sort(effs, function(a,b) return a > b end) + + local n = table.getn(effs) + local ref = 0 + if n >= 3 then + ref = effs[1] * 0.45 + effs[2] * 0.30 + effs[3] * 0.25 + elseif n == 2 then + ref = effs[1] * 0.55 + effs[2] * 0.45 + elseif n == 1 then + ref = effs[1] + end + + if ref <= 0 then ref = 1.0 end + return ref end -local function CalcRawEP(bonuses, weights) +local DPS_DAMPEN_MELEE = 0.40 +local DPS_DAMPEN_RANGED = 0.70 + +local function CalcRawEP(bonuses, weights, dpsDampen) if not bonuses or not weights then return 0 end + dpsDampen = dpsDampen or DPS_DAMPEN_MELEE local ep = 0 for stat, value in pairs(bonuses) do local w = weights[stat] if w and w > 0 then - ep = ep + value * w + if stat == "WEAPONDPS" then + ep = ep + value * w * dpsDampen + elseif stat == "WEAPONSPEED" then + local speedBonus = 3.0 - value + if speedBonus > 0 then + ep = ep + speedBonus * w + end + else + ep = ep + value * w + end end end return ep @@ -755,21 +855,106 @@ local function CalcTotalBudget(bonuses) for stat, value in pairs(bonuses) do local cost = BUDGET_COST[stat] if cost and cost > 0 then - total = total + math.abs(value) * cost + if stat == "WEAPONSPEED" then + local speedBonus = 3.0 - value + if speedBonus > 0 then + total = total + speedBonus * cost + end + else + total = total + math.abs(value) * cost + end 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) +local GS_RANGED_LOCS = { + INVTYPE_RANGED = true, INVTYPE_RANGEDRIGHT = true, INVTYPE_THROWN = true, +} - if totalBudget <= 0 or bestEff <= 0 then return 0 end +-------------------------------------------------------------------------------- +-- Horizontal comparison: reference EP for the best rare item at level/slot +-------------------------------------------------------------------------------- - local idealEP = totalBudget * bestEff - local rawScore = (rawEP / idealEP) * 10 +local SLOT_BUDGET_MOD = { + INVTYPE_HEAD = 1.0, INVTYPE_CHEST = 1.0, INVTYPE_ROBE = 1.0, INVTYPE_LEGS = 1.0, + INVTYPE_SHOULDER = 0.77, INVTYPE_HAND = 0.77, INVTYPE_WAIST = 0.77, INVTYPE_FEET = 0.77, + INVTYPE_WRIST = 0.56, INVTYPE_CLOAK = 0.56, + INVTYPE_NECK = 0.56, INVTYPE_FINGER = 0.56, INVTYPE_TRINKET = 0.56, + INVTYPE_WEAPON = 0.42, INVTYPE_WEAPONMAINHAND = 0.42, INVTYPE_WEAPONOFFHAND = 0.36, + INVTYPE_2HWEAPON = 1.0, + INVTYPE_SHIELD = 0.56, INVTYPE_HOLDABLE = 0.42, + INVTYPE_RANGED = 0.32, INVTYPE_RANGEDRIGHT = 0.32, INVTYPE_THROWN = 0.32, + INVTYPE_RELIC = 0.32, INVTYPE_TABARD = 0, INVTYPE_BODY = 0, +} + +local WEAPON_EQUIP_LOCS = { + INVTYPE_WEAPON = true, INVTYPE_WEAPONMAINHAND = true, INVTYPE_WEAPONOFFHAND = true, + INVTYPE_2HWEAPON = true, INVTYPE_RANGED = true, INVTYPE_RANGEDRIGHT = true, + INVTYPE_THROWN = true, +} + +local ARMOR_EQUIP_LOCS = { + INVTYPE_HEAD = true, INVTYPE_CHEST = true, INVTYPE_ROBE = true, INVTYPE_LEGS = true, + INVTYPE_SHOULDER = true, INVTYPE_HAND = true, INVTYPE_WAIST = true, INVTYPE_FEET = true, + INVTYPE_WRIST = true, INVTYPE_SHIELD = true, +} + +local function GetRefWeaponDPS(equipLoc, level) + local dps + if level >= 58 then dps = 48 + elseif level >= 40 then dps = 28 + (level - 40) * 0.55 + elseif level >= 20 then dps = 13 + (level - 20) * 0.75 + elseif level >= 10 then dps = 6 + (level - 10) * 0.7 + else dps = 3 + level * 0.3 + end + if equipLoc == "INVTYPE_2HWEAPON" then dps = dps * 1.3 end + return dps +end + +local function GetReferenceEP(equipLoc, level, weights, refEff) + local slotMod = SLOT_BUDGET_MOD[equipLoc] or 0.56 + + local refIlvl + if level >= 58 then refIlvl = 63 + elseif level >= 40 then refIlvl = level + 7 + elseif level >= 20 then refIlvl = level + 5 + else refIlvl = level + 3 + end + + local statBudget = refIlvl * 0.65 * slotMod + local ep = statBudget * refEff + + if WEAPON_EQUIP_LOCS[equipLoc] then + local wDPS = weights.WEAPONDPS + if wDPS and wDPS > 0 then + local refDPS = GetRefWeaponDPS(equipLoc, level) + local dampen = GS_RANGED_LOCS[equipLoc] and DPS_DAMPEN_RANGED or DPS_DAMPEN_MELEE + ep = ep + refDPS * wDPS * dampen + end + end + + if ARMOR_EQUIP_LOCS[equipLoc] then + local wArmor = weights.BASEARMOR + if wArmor and wArmor > 0 then + local refArmor = level * 1.5 * slotMod + ep = ep + refArmor * wArmor + end + end + + return ep +end + +local function CalcNormalizedScore(bonuses, weights, armorCompat, slotCompat, level, equipLoc) + local dpsDampen = DPS_DAMPEN_MELEE + if equipLoc and GS_RANGED_LOCS[equipLoc] then dpsDampen = DPS_DAMPEN_RANGED end + local rawEP = CalcRawEP(bonuses, weights, dpsDampen) + local refEff = GetRefEfficiency(weights, level) + local refEP = GetReferenceEP(equipLoc, level, weights, refEff) + + if refEP <= 0 then return 0 end + + local rawScore = (rawEP / refEP) * 10 local compat = (armorCompat or 1.0) * (slotCompat or 1.0) local finalScore = rawScore * compat @@ -800,10 +985,57 @@ local function ScoreLabel(score) return "不适" end +-------------------------------------------------------------------------------- +-- Score tooltip (uses GameTooltipTemplate — proven reliable in this addon) +-- Created lazily on first use (nil parent like Trade.lua pattern) +-------------------------------------------------------------------------------- + +local GSTip + +local function GS_ShowTip(parentTip, scoreLines) + if not scoreLines or not parentTip then return end + + if not GSTip then + GSTip = CreateFrame("GameTooltip", "NanamiGSTooltip", nil, "GameTooltipTemplate") + GSTip:SetFrameStrata("TOOLTIP") + GSTip:SetClampedToScreen(true) + end + + GSTip:SetOwner(parentTip, "ANCHOR_NONE") + GSTip:ClearAllPoints() + + local bottom = parentTip:GetBottom() + if bottom and bottom > 80 then + GSTip:SetPoint("TOPLEFT", parentTip, "BOTTOMLEFT", 0, 2) + else + GSTip:SetPoint("BOTTOMLEFT", parentTip, "TOPLEFT", 0, -2) + end + + for _, entry in ipairs(scoreLines) do + if entry.left and entry.right then + GSTip:AddDoubleLine(entry.left, entry.right, + entry.lr or 1, entry.lg or 1, entry.lb or 1, + entry.rr or 1, entry.rg or 1, entry.rb or 1) + else + GSTip:AddLine(entry.text or "", entry.r or 1, entry.g or 0.84, entry.b or 0) + end + end + + GSTip:Show() +end + +local function GS_HideFrame() + if GSTip then GSTip:Hide() end +end + -------------------------------------------------------------------------------- -- Main tooltip function -------------------------------------------------------------------------------- +local GS_SCORE_CACHE = {} +local GS_CACHE_SIZE = 0 +local GS_CACHE_MAX = 200 + function GS:AddScoreToTooltip(tooltip, link) if not tooltip or not link then return end if SFramesDB and SFramesDB.gearScore == false then return end @@ -814,9 +1046,20 @@ function GS:AddScoreToTooltip(tooltip, link) local classData = WEIGHTS[classToken] if not classData then return end + local cacheKey = classToken .. "|" .. link + local cached = GS_SCORE_CACHE[cacheKey] + if cached then + tooltip._gsScoreAdded = true + if cached.scoreLines then + GS_ShowTip(tooltip, cached.scoreLines) + else + GS_HideFrame() + end + 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) @@ -828,7 +1071,6 @@ function GS:AddScoreToTooltip(tooltip, link) 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) @@ -850,7 +1092,6 @@ function GS:AddScoreToTooltip(tooltip, link) .. " 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 @@ -861,19 +1102,15 @@ function GS:AddScoreToTooltip(tooltip, link) 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") @@ -889,7 +1126,6 @@ function GS:AddScoreToTooltip(tooltip, link) 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 @@ -900,7 +1136,8 @@ function GS:AddScoreToTooltip(tooltip, link) local isHC = IsPlayerHardcore() local armorCompat = GetArmorCompat(classToken, itemClass, itemSubClass, level) - GSDebug("ArmorCompat=" .. armorCompat .. " class=" .. classToken) + GSDebug("ArmorCompat=" .. armorCompat .. " class=" .. classToken + .. " slot=" .. tostring(equipLoc) .. " lv=" .. level) local specs = classData.specs if not specs or table.getn(specs) == 0 then return end @@ -910,7 +1147,11 @@ function GS:AddScoreToTooltip(tooltip, link) 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) + local refEff = GetRefEfficiency(w, level) + local refEP = GetReferenceEP(equipLoc, level, w, refEff) + local s = CalcNormalizedScore(bonuses, w, armorCompat, slotCompat, level, equipLoc) + GSDebug(" " .. spec.name .. ": rawScore=" .. string.format("%.2f", s) + .. " refEP=" .. string.format("%.1f", refEP) .. " refEff=" .. string.format("%.3f", refEff)) s = math.floor(s * qualityMult * 10 + 0.5) / 10 if s < 1.0 and s > 0 then s = 1.0 end table.insert(scores, { @@ -926,38 +1167,55 @@ function GS:AddScoreToTooltip(tooltip, link) local hcScore = 0 if classData.hc then local hw = AdjustWeightsForLevel(classData.hc.w, level, true) - hcScore = CalcNormalizedScore(bonuses, hw, armorCompat, 1.0) + hcScore = CalcNormalizedScore(bonuses, hw, armorCompat, 1.0, level, equipLoc) + if not bonuses.STA or bonuses.STA <= 0 then + hcScore = hcScore * 0.35 + end 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 + if not anyShow then + GS_SCORE_CACHE[cacheKey] = { scoreLines = nil } + GS_CACHE_SIZE = GS_CACHE_SIZE + 1 + GS_HideFrame() + return + end tooltip._gsScoreAdded = true - tooltip:AddLine(" ") - tooltip:AddLine("|cffffd700── 装备评分 ──|r") + local scoreLines = {} + table.insert(scoreLines, { text = "── 装备评分 ──", r = 1, g = 0.84, b = 0 }) 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) + table.insert(scoreLines, { + left = star .. "|c" .. sd.color .. sd.name .. "|r", + right = "|c" .. sColor .. sStr .. " " .. sd.label .. "|r", + }) 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) + table.insert(scoreLines, { + left = hcStar .. "|c" .. classData.hc.color .. "硬核|r", + right = "|c" .. hcSColor .. string.format("%.1f", hcScore) + .. " " .. ScoreLabel(hcScore) .. "|r", + }) end - tooltip:Show() + GS_ShowTip(tooltip, scoreLines) + + if GS_CACHE_SIZE >= GS_CACHE_MAX then + GS_SCORE_CACHE = {} + GS_CACHE_SIZE = 0 + end + GS_SCORE_CACHE[cacheKey] = { scoreLines = scoreLines } + GS_CACHE_SIZE = GS_CACHE_SIZE + 1 end -------------------------------------------------------------------------------- @@ -976,6 +1234,7 @@ function GS:HookTooltips() local origHide = GameTooltip:GetScript("OnHide") GameTooltip:SetScript("OnHide", function() this._gsScoreAdded = nil + GS_HideFrame() if origHide then origHide() end end) diff --git a/Merchant.lua b/Merchant.lua index 29f8738..2c8e98e 100644 --- a/Merchant.lua +++ b/Merchant.lua @@ -411,6 +411,7 @@ local function EnsureBuyPopup() BuyPopup:Show() editbox:SetFocus() + editbox:HighlightText() end end diff --git a/SetupWizard.lua b/SetupWizard.lua index ab3c7b2..380afd2 100644 --- a/SetupWizard.lua +++ b/SetupWizard.lua @@ -349,7 +349,7 @@ local function GetDefaultChoices() mapRevealEnabled = true, mapRevealAlpha = 0.7, worldMapEnabled = true, - hcGlobalDisable = true, + hcGlobalDisable = false, iconSet = "icon", } end diff --git a/SocialUI.lua b/SocialUI.lua index dac22c1..a70e829 100644 --- a/SocialUI.lua +++ b/SocialUI.lua @@ -61,6 +61,63 @@ local function BuildClassReverseLookup() end end +-------------------------------------------------------------------------------- +-- Chinese -> English search-term translation for SendWho +-------------------------------------------------------------------------------- +local WHO_ZH_TO_EN = { + -- Classes + ["战士"] = "Warrior", ["法师"] = "Mage", ["盗贼"] = "Rogue", + ["德鲁伊"] = "Druid", ["猎人"] = "Hunter", ["萨满祭司"] = "Shaman", + ["萨满"] = "Shaman", ["牧师"] = "Priest", ["术士"] = "Warlock", + ["圣骑士"] = "Paladin", + -- Races + ["人类"] = "Human", ["矮人"] = "Dwarf", ["暗夜精灵"] = "Night Elf", + ["侏儒"] = "Gnome", ["兽人"] = "Orc", ["巨魔"] = "Troll", + ["亡灵"] = "Undead", ["牛头人"] = "Tauren", ["高等精灵"] = "High Elf", + ["哥布林"] = "Goblin", + -- Zones (Alliance) + ["暴风城"] = "Stormwind", ["铁炉堡"] = "Ironforge", ["达纳苏斯"] = "Darnassus", + ["艾尔文森林"] = "Elwynn Forest", ["西部荒野"] = "Westfall", + ["丹莫罗"] = "Dun Morogh", ["洛克莫丹"] = "Loch Modan", + ["湿地"] = "Wetlands", ["赤脊山"] = "Redridge Mountains", + ["暮色森林"] = "Duskwood", ["荆棘谷"] = "Stranglethorn Vale", + ["泰达希尔"] = "Teldrassil", ["黑海岸"] = "Darkshore", + ["灰谷"] = "Ashenvale", ["石爪山脉"] = "Stonetalon Mountains", + -- Zones (Horde) + ["奥格瑞玛"] = "Orgrimmar", ["雷霆崖"] = "Thunder Bluff", + ["幽暗城"] = "Undercity", ["杜隆塔尔"] = "Durotar", + ["莫高雷"] = "Mulgore", ["贫瘠之地"] = "The Barrens", + ["银松森林"] = "Silverpine Forest", ["提瑞斯法林地"] = "Tirisfal Glades", + ["希尔斯布莱德丘陵"] = "Hillsbrad Foothills", + -- Zones (Contested / High-level) + ["塔纳利斯"] = "Tanaris", ["菲拉斯"] = "Feralas", + ["凄凉之地"] = "Desolace", ["尘泥沼泽"] = "Dustwallow Marsh", + ["千针石林"] = "Thousand Needles", ["辛特兰"] = "The Hinterlands", + ["阿拉希高地"] = "Arathi Highlands", ["荒芜之地"] = "Badlands", + ["灼热峡谷"] = "Searing Gorge", ["燃烧平原"] = "Burning Steppes", + ["西瘟疫之地"] = "Western Plaguelands", ["东瘟疫之地"] = "Eastern Plaguelands", + ["费伍德森林"] = "Felwood", ["冬泉谷"] = "Winterspring", + ["安戈洛环形山"] = "Un'Goro Crater", ["希利苏斯"] = "Silithus", + ["艾萨拉"] = "Azshara", ["诅咒之地"] = "Blasted Lands", + ["逆风小径"] = "Deadwind Pass", ["悲伤沼泽"] = "Swamp of Sorrows", + -- Dungeons / Raids + ["熔火之心"] = "Molten Core", ["黑翼之巢"] = "Blackwing Lair", + ["奥妮克希亚的巢穴"] = "Onyxia's Lair", ["祖尔格拉布"] = "Zul'Gurub", + ["安其拉"] = "Ahn'Qiraj", ["纳克萨玛斯"] = "Naxxramas", + ["黑石深渊"] = "Blackrock Depths", ["黑石塔"] = "Blackrock Spire", + ["斯坦索姆"] = "Stratholme", ["通灵学院"] = "Scholomance", + ["厄运之槌"] = "Dire Maul", ["玛拉顿"] = "Maraudon", + ["祖尔法拉克"] = "Zul'Farrak", +} + +local function TranslateWhoQuery(text) + if not text then return "" end + for zh, en in pairs(WHO_ZH_TO_EN) do + text = string.gsub(text, zh, en) + end + return text +end + local CLASS_ICON_PATH = "Interface\\AddOns\\Nanami-UI\\img\\UI-Classes-Circles" local CLASS_ICON_TCOORDS = { ["WARRIOR"] = { 0, 0.25, 0, 0.25 }, @@ -446,6 +503,12 @@ local function CreateScrollArea(parent, w, h) return container end +local function WhoDebug(msg) + if DEFAULT_CHAT_FRAME then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ffcc[Who调试]|r " .. msg) + end +end + -------------------------------------------------------------------------------- -- Hide Blizzard FriendsFrame -------------------------------------------------------------------------------- @@ -458,6 +521,55 @@ local function HideBlizzardFriends() FriendsFrame:ClearAllPoints() FriendsFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) FriendsFrame.Show = function() end + + if SetWhoToUI then + local origSetWhoToUI = SetWhoToUI + SetWhoToUI = function(flag) + if flag ~= 1 then + WhoDebug("拦截 SetWhoToUI(" .. tostring(flag) .. ") -> 强制为1") + end + origSetWhoToUI(1) + end + end +end + +-------------------------------------------------------------------------------- +-- Who query helper +-------------------------------------------------------------------------------- +local whoQueryPending = false +local whoTimeoutFrame = nil + +local function DoSendWho(query) + if whoQueryPending and whoTimeoutFrame then + WhoDebug("取消上次挂起的查询") + whoQueryPending = false + whoTimeoutFrame:SetScript("OnUpdate", nil) + end + + WhoDebug("发送查询: \"" .. (query or "") .. "\"") + if SetWhoToUI then SetWhoToUI(1) end + whoQueryPending = true + SendWho(query or "") + WhoDebug("SendWho() 已调用, 等待 WHO_LIST_UPDATE...") + + if not whoTimeoutFrame then + whoTimeoutFrame = CreateFrame("Frame", nil, UIParent) + end + whoTimeoutFrame.elapsed = 0 + whoTimeoutFrame:SetScript("OnUpdate", function() + this.elapsed = (this.elapsed or 0) + (arg1 or 0.016) + if not whoQueryPending then + this:SetScript("OnUpdate", nil) + return + end + if this.elapsed >= 6 then + local n = GetNumWhoResults() + WhoDebug("超时! 6秒未收到事件, 强制刷新, 当前结果=" .. tostring(n)) + whoQueryPending = false + this:SetScript("OnUpdate", nil) + SUI:UpdateWhoList() + end + end) end -------------------------------------------------------------------------------- @@ -902,20 +1014,54 @@ local function BuildWhoPage(page) searchBar:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) searchBar:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, 0) - local editBox = MakeEditBox(searchBar, CONTENT_W - 70, 22) + local editBox = MakeEditBox(searchBar, CONTENT_W - 110, 22) editBox:SetPoint("LEFT", searchBar, "LEFT", 0, 0) - editBox:SetScript("OnEnterPressed", function() + + local placeholder = editBox:CreateFontString(nil, "ARTWORK") + placeholder:SetFont(GetFont(), 10, "OUTLINE") + placeholder:SetPoint("LEFT", editBox, "LEFT", 6, 0) + placeholder:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3], 0.6) + placeholder:SetText("名称/等级/职业/种族/区域") + editBox.placeholder = placeholder + + editBox:SetScript("OnTextChanged", function() local text = this:GetText() - if text and text ~= "" then SendWho(text) end + if text and text ~= "" then + this.placeholder:Hide() + else + this.placeholder:Show() + end + end) + editBox:SetScript("OnEditFocusGained", function() + if this:GetText() == "" then this.placeholder:Show() end + end) + editBox:SetScript("OnEditFocusLost", function() + if this:GetText() == "" then this.placeholder:Show() end + end) + + editBox:SetScript("OnEnterPressed", function() + local text = this:GetText() or "" + SUI:ClearWhoList() + DoSendWho(text) this:ClearFocus() end) page.editBox = editBox + local clearBtn = MakeButton(searchBar, "X", 28, 22) + clearBtn:SetPoint("LEFT", editBox, "RIGHT", 2, 0) + clearBtn:SetScript("OnClick", function() + if page.editBox then + page.editBox:SetText("") + page.editBox:SetFocus() + end + end) + local searchBtn = MakeButton(searchBar, "搜索", 64, 22) - searchBtn:SetPoint("LEFT", editBox, "RIGHT", 4, 0) + searchBtn:SetPoint("LEFT", clearBtn, "RIGHT", 2, 0) searchBtn:SetScript("OnClick", function() - local text = page.editBox:GetText() - if text and text ~= "" then SendWho(text) end + local text = page.editBox:GetText() or "" + SUI:ClearWhoList() + DoSendWho(text) end) -- Column headers @@ -935,12 +1081,13 @@ local function BuildWhoPage(page) end MakeSep(page, -46) - -- Results scroll + -- Results scroll (leave 16px above btnBar for totalFS) local listArea = CreateFrame("Frame", nil, page) listArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -48) - listArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, BOTTOM_H) + listArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, BOTTOM_H + 18) - local wScroll = CreateScrollArea(listArea, CONTENT_W, FRAME_H - HEADER_H - TAB_BAR_H - 48 - BOTTOM_H - 16) + local scrollH = FRAME_H - HEADER_H - TAB_BAR_H - 48 - BOTTOM_H - 18 + local wScroll = CreateScrollArea(listArea, CONTENT_W, scrollH) wScroll:SetPoint("TOPLEFT", listArea, "TOPLEFT", 0, 0) page.wScroll = wScroll @@ -1032,6 +1179,27 @@ local function BuildWhoPage(page) end) end +function SUI:ClearWhoList() + selectedWho = nil + for i = 1, 50 do + local row = whoRows[i] + if not row then break end + row.nameFS:SetText("") + row.lvlFS:SetText("") + row.classFS:SetText("") + row.zoneFS:SetText("") + SetRowNormal(row) + HideSelHighlight(row) + row:Hide() + end + if pages[2] and pages[2].wScroll then + pages[2].wScroll:SetContentHeight(4) + end + if pages[2] and pages[2].totalFS then + pages[2].totalFS:SetText("搜索中...") + end +end + function SUI:UpdateWhoList() local numWho, totalCount = GetNumWhoResults() local totalH = 0 @@ -1120,11 +1288,12 @@ local function BuildGuildPage(page) end) page.gSearchBox = gSearchBox - local offlineToggle = MakeButton(toolBar, "隐藏离线", 56, 16) + local offlineToggle = MakeButton(toolBar, guildHideOffline and "显示离线" or "隐藏离线", 56, 16) offlineToggle:SetPoint("TOPRIGHT", toolBar, "TOPRIGHT", 0, 0) offlineToggle.text:SetFont(GetFont(), 9, "OUTLINE") offlineToggle:SetScript("OnClick", function() guildHideOffline = not guildHideOffline + SFramesDB.guildHideOffline = guildHideOffline if guildHideOffline then this.text:SetText("显示离线") else @@ -2099,6 +2268,7 @@ local function ShowPage(tabIdx) if origShowFriendsAPI then origShowFriendsAPI() end SUI:UpdateFriendsPage() elseif tabIdx == 2 then + if SetWhoToUI then SetWhoToUI(1) end SUI:UpdateWhoList() elseif tabIdx == 3 then SUI:UpdateGuildList() @@ -2108,7 +2278,16 @@ local function ShowPage(tabIdx) if MainFrame and MainFrame.titleFS then local titles = { "好友名单", "查询玩家", "公会", "团队" } - MainFrame.titleFS:SetText(titles[tabIdx] or "社交") + if tabIdx == 3 and IsInGuild() then + local guildName = GetGuildInfo("player") + if guildName and guildName ~= "" then + MainFrame.titleFS:SetText("< " .. guildName .. " >") + else + MainFrame.titleFS:SetText(titles[tabIdx]) + end + else + MainFrame.titleFS:SetText(titles[tabIdx] or "社交") + end end end @@ -2260,6 +2439,8 @@ function SUI:Initialize() if initialized then return end initialized = true + guildHideOffline = SFramesDB.guildHideOffline or false + BuildClassReverseLookup() HideBlizzardFriends() BuildMainFrame() @@ -2272,11 +2453,29 @@ function SUI:Initialize() ef:RegisterEvent("RAID_ROSTER_UPDATE") ef:RegisterEvent("PARTY_MEMBERS_CHANGED") ef:SetScript("OnEvent", function() + if event == "WHO_LIST_UPDATE" then + local n, t = GetNumWhoResults() + WhoDebug("收到 WHO_LIST_UPDATE! 结果=" .. tostring(n) .. " 总计=" .. tostring(t) .. " pending=" .. tostring(whoQueryPending)) + local wasPending = whoQueryPending + whoQueryPending = false + if SetWhoToUI then SetWhoToUI(1) end + if MainFrame and MainFrame:IsShown() and currentMainTab == 2 then + WhoDebug("更新列表显示") + SUI:UpdateWhoList() + else + WhoDebug("自动打开查询页显示结果") + if not MainFrame then BuildMainFrame() end + if MainFrame then + MainFrame:Show() + ShowPage(2) + SUI:UpdateWhoList() + end + end + return + end 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 @@ -2314,6 +2513,46 @@ if ShowFriends then end end +-------------------------------------------------------------------------------- +-- Hook SetItemRef: shift-click player name -> WHO query in our panel +-------------------------------------------------------------------------------- +do + local origSetItemRef_SUI = SetItemRef + SetItemRef = function(link, text, button) + if link and IsShiftKeyDown and IsShiftKeyDown() then + local playerName = nil + if string.sub(link, 1, 7) == "player:" then + playerName = string.sub(link, 8) + local colonPos = string.find(playerName, ":") + if colonPos then playerName = string.sub(playerName, 1, colonPos - 1) end + end + if playerName and playerName ~= "" then + if SFramesDB and SFramesDB.enableSocial ~= false and initialized then + WhoDebug("Shift点击玩家: " .. playerName .. ", 发起WHO查询") + local query = "n-\"" .. playerName .. "\"" + if not MainFrame then BuildMainFrame() end + if MainFrame then + MainFrame:Show() + ShowPage(2) + end + if pages[2] and pages[2].editBox then + pages[2].editBox:SetText(playerName) + if pages[2].editBox.placeholder then + pages[2].editBox.placeholder:Hide() + end + end + SUI:ClearWhoList() + DoSendWho(query) + return + end + end + end + if origSetItemRef_SUI then + origSetItemRef_SUI(link, text, button) + end + end +end + -------------------------------------------------------------------------------- -- Bootstrap -------------------------------------------------------------------------------- diff --git a/Trade.lua b/Trade.lua index 6e2bdd2..5881c4e 100644 --- a/Trade.lua +++ b/Trade.lua @@ -177,11 +177,18 @@ local function SendTradeWhisper() 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 + local useCN = (SFramesDB.TradeWhisperLang == "ZH") + local header = useCN and "=== 交易完成清单 ===" or "=== Trade Summary ===" + local lblGiveG = useCN and "我方金币: " or "I gave gold: " + local lblGiveI = useCN and "我方物品: " or "I gave items: " + local lblGotG = useCN and "对方金币: " or "I got gold: " + local lblGotI = useCN and "对方物品: " or "I got items: " + + table.insert(outLines, header) + if playerMoneyStr then table.insert(outLines, lblGiveG .. playerMoneyStr) end + if giveItems ~= "" then table.insert(outLines, lblGiveI .. giveItems) end + if targetMoneyStr then table.insert(outLines, lblGotG .. targetMoneyStr) end + if getItems ~= "" then table.insert(outLines, lblGotI .. getItems) end for _, line in ipairs(outLines) do SendLine(line, channel, target) @@ -195,11 +202,14 @@ local function ClearTradeData() TRADE_DATA.targetMoney = 0 end +local tradeWhisperSent = false + 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 + if string.find(msg, "交易完成") then return true end + if string.find(msg, "交易成功") then return true end return false end @@ -216,6 +226,7 @@ end TradeUI:SetScript("OnEvent", function() if event == "TRADE_SHOW" then + tradeWhisperSent = false TRADE_DATA.active = true TRADE_DATA.targetName = UnitName("NPC") or "" TRADE_DATA.playerItems = {} @@ -243,16 +254,23 @@ TradeUI:SetScript("OnEvent", function() SaveTradeState() ForceRefreshTradeVisuals() elseif event == "TRADE_CLOSED" then + if TRADE_DATA.playerAccepted and TRADE_DATA.targetAccepted and not tradeWhisperSent then + tradeWhisperSent = true + SendTradeWhisper() + ClearTradeData() + end TRADE_DATA.active = false TRADE_DATA.playerAccepted = false TRADE_DATA.targetAccepted = false elseif event == "UI_INFO_MESSAGE" then - if IsTradeCompleteMsg(arg1) then + if IsTradeCompleteMsg(arg1) and not tradeWhisperSent then + tradeWhisperSent = true SendTradeWhisper() ClearTradeData() end elseif event == "CHAT_MSG_SYSTEM" then - if IsTradeCompleteMsg(arg1) then + if IsTradeCompleteMsg(arg1) and not tradeWhisperSent then + tradeWhisperSent = true SendTradeWhisper() ClearTradeData() end @@ -305,7 +323,7 @@ local function ScanItemLevelFromTooltip() 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 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 @@ -1025,17 +1043,19 @@ local function SkinTradeFrame() SFramesDB = SFramesDB or {} local selected = SFramesDB.TradeWhisperChannel or "WHISPER" for _, info in ipairs(channels) do + local capturedText = info.text + local capturedValue = info.value local d = {} - d.text = info.text - d.value = info.value + d.text = capturedText + d.value = capturedValue d.func = function() SFramesDB = SFramesDB or {} - SFramesDB.TradeWhisperChannel = this.value - UIDropDownMenu_SetSelectedValue(drop, this.value) + SFramesDB.TradeWhisperChannel = capturedValue + UIDropDownMenu_SetSelectedValue(drop, capturedValue) local txt = _G[drop:GetName() .. "Text"] - if txt then txt:SetText(info.text) end + if txt then txt:SetText(capturedText) end end - d.checked = (info.value == selected) + d.checked = (capturedValue == selected) UIDropDownMenu_AddButton(d) end end @@ -1051,6 +1071,55 @@ local function SkinTradeFrame() end end end + + -- Language dropdown (EN / ZH) + local langDrop = CreateFrame("Frame", "SFramesTradeLangObj", TradeFrame, "UIDropDownMenuTemplate") + langDrop:SetPoint("LEFT", drop, "RIGHT", -16, 0) + UIDropDownMenu_SetWidth(50, langDrop) + + local langDropText = _G[langDrop:GetName() .. "Text"] + if langDropText then + langDropText:SetFont(GetFont(), 10, "OUTLINE") + langDropText:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + end + + local langs = { + { text = "EN", value = "EN" }, + { text = "中文", value = "ZH" }, + } + + local function TradeLangDropInit() + SFramesDB = SFramesDB or {} + local selected = SFramesDB.TradeWhisperLang or "EN" + for _, info in ipairs(langs) do + local capText = info.text + local capValue = info.value + local d = {} + d.text = capText + d.value = capValue + d.func = function() + SFramesDB = SFramesDB or {} + SFramesDB.TradeWhisperLang = capValue + UIDropDownMenu_SetSelectedValue(langDrop, capValue) + local txt = _G[langDrop:GetName() .. "Text"] + if txt then txt:SetText(capText) end + end + d.checked = (capValue == selected) + UIDropDownMenu_AddButton(d) + end + end + + UIDropDownMenu_Initialize(langDrop, TradeLangDropInit) + SFramesDB = SFramesDB or {} + UIDropDownMenu_SetSelectedValue(langDrop, SFramesDB.TradeWhisperLang or "EN") + if langDropText then + for _, info in ipairs(langs) do + if info.value == (SFramesDB.TradeWhisperLang or "EN") then + langDropText:SetText(info.text) + break + end + end + end end -- Close button diff --git a/TrainerUI.lua b/TrainerUI.lua index 53109cb..d040ab3 100644 --- a/TrainerUI.lua +++ b/TrainerUI.lua @@ -333,6 +333,18 @@ local function CreateActionBtn(parent, text, w) return btn end +-------------------------------------------------------------------------------- +-- Quality cache (lazy per-row instead of bulk tooltip scan) +-------------------------------------------------------------------------------- +local qualityCache = {} + +local function GetCachedServiceQuality(index) + if qualityCache[index] ~= nil then return qualityCache[index] end + local q = GetServiceQuality(index) + qualityCache[index] = q or false + return q +end + -------------------------------------------------------------------------------- -- List Row Factory (reusable for both headers and services) -------------------------------------------------------------------------------- @@ -530,8 +542,9 @@ local function CreateListRow(parent, idx) 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 + local quality = GetCachedServiceQuality(svc.index) + 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.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1) @@ -607,7 +620,6 @@ local function BuildDisplayList() name = name, subText = subText or "", category = category or "unavailable", - quality = GetServiceQuality(i), }) end end @@ -1172,19 +1184,22 @@ function TUI:Initialize() selectedIndex = nil currentFilter = "all" collapsedCats = {} + qualityCache = {} local npcName = UnitName("npc") or "训练师" if IsTradeskillTrainer and IsTradeskillTrainer() then npcName = npcName .. " - 专业训练" end MainFrame.npcNameFS:SetText(npcName) MainFrame:Show() - FullUpdate() + BuildDisplayList() for _, entry in ipairs(displayList) do if entry.type == "service" then - SelectService(entry.data.index) + selectedIndex = entry.data.index + pcall(SelectTrainerService, entry.data.index) break end end + FullUpdate() MainFrame._hideBlizzTimer = 0 MainFrame:SetScript("OnUpdate", function() diff --git a/Units/Player.lua b/Units/Player.lua index 8515e17..36f1981 100644 --- a/Units/Player.lua +++ b/Units/Player.lua @@ -438,6 +438,36 @@ function SFrames.Player:Initialize() self:ShowTrainerReminder(arg1) end end) + SFrames:RegisterEvent("TRAINER_SHOW", function() + SFrames.Player.trainerScannedThisVisit = nil + SFrames.Player.trainerShowPending = true + SFrames.Player.trainerRetryCount = 0 + if not SFrames.Player.trainerRetryFrame then + SFrames.Player.trainerRetryFrame = CreateFrame("Frame") + end + SFrames.Player.trainerRetryFrame:SetScript("OnUpdate", function() + if not this.elapsed then this.elapsed = 0 end + this.elapsed = this.elapsed + arg1 + if this.elapsed < 0.3 then return end + this.elapsed = 0 + if SFrames.Player.trainerScannedThisVisit then + this:SetScript("OnUpdate", nil) + return + end + SFrames.Player.trainerRetryCount = (SFrames.Player.trainerRetryCount or 0) + 1 + if SFrames.Player.trainerRetryCount > 10 then + SFrames.Player:ScanTrainer() + this:SetScript("OnUpdate", nil) + return + end + SFrames.Player:ScanTrainer() + end) + end) + SFrames:RegisterEvent("TRAINER_UPDATE", function() + if not SFrames.Player.trainerScannedThisVisit then + SFrames.Player:ScanTrainer() + 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) @@ -458,11 +488,13 @@ function SFrames.Player:Initialize() end function SFrames.Player:HasSpellInBook(spellName) + local baseName = string.gsub(spellName, " 等级 %d+$", "") + baseName = string.gsub(baseName, " %d+级$", "") 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 + if name == spellName or name == baseName then return true end i = i + 1 end end @@ -483,6 +515,167 @@ function SFrames.Player:GetSpellIcon(skillDisplayName) return nil end +function SFrames.Player:ParseTrainerTooltipLevel(serviceIndex) + if not self.trainerScanTip then + local tt = CreateFrame("GameTooltip", "NanamiTrainerScanTip", nil, "GameTooltipTemplate") + tt:SetOwner(UIParent, "ANCHOR_NONE") + self.trainerScanTip = tt + end + local tt = self.trainerScanTip + tt:ClearLines() + if not tt.SetTrainerService then return nil end + tt:SetTrainerService(serviceIndex) + for j = 2, tt:NumLines() do + local textObj = getglobal("NanamiTrainerScanTipTextLeft" .. j) + if textObj then + local text = textObj:GetText() + if text then + local _, _, lvl = string.find(text, "需要等级%s*(%d+)") + if not lvl then + _, _, lvl = string.find(text, "Requires Level (%d+)") + end + if lvl then return tonumber(lvl) end + end + end + end + return nil +end + +function SFrames.Player:FindSkillLevelInStaticData(classEn, skillName) + local staticData = SFrames.ClassSkillData and SFrames.ClassSkillData[classEn] + if not staticData then return nil end + local baseName = string.gsub(skillName, " %d+级$", "") + baseName = string.gsub(baseName, "(等级 %d+)$", "") + baseName = string.gsub(baseName, "%s+$", "") + for level, skills in pairs(staticData) do + for _, s in ipairs(skills) do + local sBase = string.gsub(s, " %d+级$", "") + sBase = string.gsub(sBase, "(等级 %d+)$", "") + sBase = string.gsub(sBase, "%s+$", "") + if baseName == sBase or skillName == s then + return level + end + end + end + local talentData = SFrames.TalentTrainerSkills and SFrames.TalentTrainerSkills[classEn] + if talentData then + for level, entries in pairs(talentData) do + for _, entry in ipairs(entries) do + local tBase = string.gsub(entry[1], " %d+级$", "") + if baseName == tBase or skillName == entry[1] then + return level + end + end + end + end + return nil +end + +function SFrames.Player:ScanTrainer() + if self.scanningTrainer then return end + self.scanningTrainer = true + + local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9" + + if not GetNumTrainerServices then + DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff6666训练师扫描失败: GetNumTrainerServices 不存在|r") + self.scanningTrainer = nil + return + end + if IsTradeskillTrainer and IsTradeskillTrainer() then + self.scanningTrainer = nil + return + end + + local _, classEn = UnitClass("player") + if not classEn or not SFramesDB then self.scanningTrainer = nil return end + + local cache = {} + local numServices = GetNumTrainerServices() + if not numServices or numServices == 0 then + DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff6666训练师扫描: 未检测到技能列表 (0项),将在数据加载后重试|r") + self.scanningTrainer = nil + return + end + + local hasLevelAPI = (GetTrainerServiceLevelReq ~= nil) + local noLevelCount = 0 + local totalAdded = 0 + + local iconMissCount = 0 + + for i = 1, numServices do + local name, subText, serviceType = GetTrainerServiceInfo(i) + + if name and subText and subText ~= "" then + local icon = GetTrainerServiceIcon and GetTrainerServiceIcon(i) + + if (not icon or icon == "") and ClassTrainerFrame then + local skillButton = getglobal("ClassTrainerSkill" .. i) + if skillButton then + local iconTex = getglobal("ClassTrainerSkill" .. i .. "Icon") + if iconTex and iconTex.GetTexture then + icon = iconTex:GetTexture() + end + end + end + + if not icon or icon == "" then + iconMissCount = iconMissCount + 1 + end + + local reqLevel + if hasLevelAPI then + reqLevel = GetTrainerServiceLevelReq(i) + end + if not reqLevel or reqLevel == 0 then + reqLevel = self:ParseTrainerTooltipLevel(i) + end + if not reqLevel or reqLevel == 0 then + local lookupName = name .. " " .. subText + reqLevel = self:FindSkillLevelInStaticData(classEn, lookupName) + if not reqLevel then + reqLevel = self:FindSkillLevelInStaticData(classEn, name) + end + end + if not reqLevel or reqLevel == 0 then + noLevelCount = noLevelCount + 1 + end + + if reqLevel and reqLevel > 0 then + local displayName = name .. " " .. subText + if not cache[reqLevel] then + cache[reqLevel] = {} + end + table.insert(cache[reqLevel], { + name = displayName, + icon = icon or "", + }) + totalAdded = totalAdded + 1 + end + end + end + + if not SFramesDB.trainerCache then + SFramesDB.trainerCache = {} + end + SFramesDB.trainerCache[classEn] = cache + self.trainerScannedThisVisit = true + + local levelCount = 0 + for _ in pairs(cache) do levelCount = levelCount + 1 end + local iconOk = totalAdded - iconMissCount + DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r 已从训练师更新技能数据(" .. levelCount .. " 个等级," .. totalAdded .. " 项技能," .. iconOk .. " 个图标)") + if noLevelCount > 0 then + DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff9900有 " .. noLevelCount .. " 项技能无法确定等级要求,已跳过|r") + end + if iconMissCount > 0 then + DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff9900有 " .. iconMissCount .. " 项技能未获取到图标|r") + end + + self.scanningTrainer = nil +end + function SFrames.Player:ShowTrainerReminder(newLevel) local _, classEn = UnitClass("player") local classNames = { @@ -507,25 +700,49 @@ function SFrames.Player:ShowTrainerReminder(newLevel) local allSkills = {} local allIcons = {} + local talentSkills = {} + local usedCache = false - 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) + local classCache = SFramesDB and SFramesDB.trainerCache and SFramesDB.trainerCache[classEn] + if classCache then + local lowLevel = newLevel - 1 + if lowLevel < 1 then lowLevel = 1 end + for lv = lowLevel, newLevel do + if classCache[lv] then + usedCache = true + for _, entry in ipairs(classCache[lv]) do + if not self:HasSpellInBook(entry.name) then + table.insert(allSkills, entry.name) + local ico = entry.icon + if not ico or ico == "" then + ico = self:GetSpellIcon(entry.name) or fallbackIcon + end + table.insert(allIcons, ico) + end + end + end 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) + if not usedCache then + 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] + 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 end @@ -533,6 +750,11 @@ function SFrames.Player:ShowTrainerReminder(newLevel) local mountQuest = SFrames.ClassMountQuests and SFrames.ClassMountQuests[classEn] and SFrames.ClassMountQuests[classEn][newLevel] local skillCount = table.getn(allSkills) + + if skillCount == 0 and not mountQuest then + return + end + local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9" SFrames:Print(string.format("已达到 %d 级!你的%s训练师有新技能可以学习。", newLevel, className)) @@ -551,7 +773,7 @@ function SFrames.Player:ShowTrainerReminder(newLevel) end end end - if table.getn(talentSkills) > 0 then + if not usedCache and table.getn(talentSkills) > 0 then DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cff00ff00(天赋)|r " .. table.concat(talentSkills, ", ")) end if mountQuest then @@ -648,6 +870,7 @@ function SFrames.Player:ShowTrainerReminder(newLevel) end local iconCount = 0 + for i = 1, skillCount do if iconCount >= 13 then break end iconCount = iconCount + 1 @@ -662,28 +885,32 @@ function SFrames.Player:ShowTrainerReminder(newLevel) 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 = "" + local preview = "" if skillCount > 0 then - countText = skillCount .. " 项技能" + preview = "|cffffd100" .. allSkills[1] .. "|r" + if skillCount > 1 then preview = preview .. ", |cffffd100" .. allSkills[2] .. "|r" end + if skillCount > 2 then preview = preview .. ", |cffffd100" .. allSkills[3] .. "|r" end + if skillCount > 3 then preview = preview .. " 等 " .. skillCount .. " 项" end end if mountQuest then - if countText ~= "" then countText = countText .. " + " end - countText = countText .. mountQuest + if preview ~= "" then preview = preview .. " | " end + preview = preview .. "|cffffff00" .. mountQuest .. "|r" end - fr.subtitle:SetText(countText) + fr.subtitle:SetText(preview) fr.detail:SetText("详见聊天窗口") else fr.subtitle:SetText("") fr.detail:SetText("前往职业训练师查看可学习的技能") end + + if iconCount > 0 then + fr:SetHeight(106) + else + fr:SetHeight(80) + end fr:Show() fr:SetAlpha(0) diff --git a/Whisper.lua b/Whisper.lua index 556bc0d..f043fbe 100644 --- a/Whisper.lua +++ b/Whisper.lua @@ -857,6 +857,31 @@ function SFrames.Whisper:Toggle() end end +-- Filter out addon sync/data messages that abuse the whisper channel. +-- These are not real conversations and should not appear in the whisper UI. +local ADDON_WHISPER_PATTERNS = { + "^%[%u+:%u+%]", -- [ST:HB], [GS:REQ], etc. + "^%u+:%u+:%d", -- ADDON:CMD:data + "^<.->.+", -- data + "^%$%u+%$", -- $ADDON$ + "^##%u+##", -- ##ADDON## + "^{.-}", -- {json-like data} + "^LVGS:", -- LevelGearSync + "^EEQ:", -- EquipExchange + "^GS:%d", -- GearScore sync + "^ST:%u", -- SpellTimer +} + +local function IsAddonWhisper(text) + if not text or text == "" then return false end + for _, pat in ipairs(ADDON_WHISPER_PATTERNS) do + if string.find(text, pat) then return true end + end + -- Pure numeric data (e.g. "30343.996") with no real words + if string.find(text, "^[%d%.%s%-]+$") then return true end + return false +end + -- Hook Events local eventFrame = CreateFrame("Frame") eventFrame:RegisterEvent("CHAT_MSG_WHISPER") @@ -871,6 +896,7 @@ eventFrame:SetScript("OnEvent", function() local text = arg1 local sender = arg2 if not sender or sender == "" then return end + if IsAddonWhisper(text) then return end sender = string.gsub(sender, "-.*", "") -- remove realm name if attached diff --git a/WorldMap.lua b/WorldMap.lua index 3cd2b80..dfcf4aa 100644 --- a/WorldMap.lua +++ b/WorldMap.lua @@ -1259,6 +1259,7 @@ end -------------------------------------------------------------------------------- local function TransformMapLinks(text) if not text or type(text) ~= "string" then return text end + if not string.find(text, "]*)>", function(c, z, x, y, name) local display = name