修改优化等

This commit is contained in:
rucky
2026-03-18 02:01:36 +08:00
parent 2a55dd6dad
commit 923a1f9ce2
15 changed files with 1578 additions and 371 deletions

View File

@@ -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,7 +1474,9 @@ function AFK:Initialize()
AFK._lastActivity = GetTime()
end
-- Hook action bar usage
if not self._hooked then
self._hooked = true
local origUseAction = UseAction
UseAction = function(a1, a2, a3)
MarkActive()
@@ -1443,8 +1498,8 @@ function AFK:Initialize()
return origJump()
end
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

View File

@@ -3109,6 +3109,27 @@ function CP:BuildSkillsPage()
page.skillRows = {}
end
do
local TRADE_HEADERS = { ["Trade Skills"] = true, ["专业技能"] = true, ["Professions"] = true,
["商业技能"] = true, ["专业"] = true }
local pendingAbandonIndex
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
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
@@ -3121,13 +3142,16 @@ function CP:UpdateSkills()
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
if GetSkillLineInfo then sn, isH, isE, sr, nt, sm, smr = GetSkillLineInfo(i) end
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)
@@ -3146,6 +3170,9 @@ function CP:UpdateSkills()
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)
@@ -3156,27 +3183,63 @@ function CP:UpdateSkills()
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: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", 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) * Clamp((sr or 0) / smr, 0, 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
end
--------------------------------------------------------------------------------
-- Tab 4: Honor (was Tab 5)

167
Chat.lua
View File

@@ -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

View File

@@ -93,6 +93,7 @@ SFrames.ClassSkillData = {
[60] = {"治疗宠物 7级", "奥术射击 8级", "扰乱射击 6级", "冰冻陷阱 3级", "摔绊 3级"},
},
ROGUE = {
[2] = {"潜行"},
[4] = {"背刺", "搜索"},
[6] = {"邪恶攻击 2级", "凿击"},
[8] = {"刺骨 2级", "闪避"},

View File

@@ -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)

View File

@@ -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,
}

View File

@@ -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,27 +785,67 @@ 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
if not EFF_EXCLUDE[stat] then
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
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
return best
end
local function CalcRawEP(bonuses, weights)
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 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
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
end
@@ -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
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)

View File

@@ -411,6 +411,7 @@ local function EnsureBuyPopup()
BuyPopup:Show()
editbox:SetFocus()
editbox:HighlightText()
end
end

View File

@@ -349,7 +349,7 @@ local function GetDefaultChoices()
mapRevealEnabled = true,
mapRevealAlpha = 0.7,
worldMapEnabled = true,
hcGlobalDisable = true,
hcGlobalDisable = false,
iconSet = "icon",
}
end

View File

@@ -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,9 +2278,18 @@ local function ShowPage(tabIdx)
if MainFrame and MainFrame.titleFS then
local titles = { "好友名单", "查询玩家", "公会", "团队" }
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
--------------------------------------------------------------------------------
-- Build Main Frame
@@ -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
--------------------------------------------------------------------------------

View File

@@ -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

View File

@@ -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()

View File

@@ -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,7 +700,31 @@ function SFrames.Player:ShowTrainerReminder(newLevel)
local allSkills = {}
local allIcons = {}
local talentSkills = {}
local usedCache = false
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
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
@@ -517,7 +734,6 @@ function SFrames.Player:ShowTrainerReminder(newLevel)
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]
@@ -529,10 +745,16 @@ function SFrames.Player:ShowTrainerReminder(newLevel)
end
end
end
end
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)

View File

@@ -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
"^<.->.+", -- <AddonName>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

View File

@@ -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, "<npin:", 1, true) then return text end
local result = string.gsub(text, "<npin:(%d+):(%d+):([%d%.]+):([%d%.]+):([^>]*)>",
function(c, z, x, y, name)
local display = name