修复猎人野兽训练展示技能所需训练点数错误问题
修复拾取问题 修复其他已知问题 修复自动下马问题以及带来的猎人守护自动关闭问题 彻底修复拾取界面问题 修复目标框架的施法条监控 修复其他已知问题 调整dps插件对仇恨的估算方式 优化dps插件 修复远程攻击条问题
This commit is contained in:
264
Chat.lua
264
Chat.lua
@@ -374,12 +374,23 @@ function SFrames:RefreshClassColorCache()
|
||||
PersistClassCache()
|
||||
end
|
||||
|
||||
SFrames._classMissCache = {}
|
||||
SFrames._classMissCacheTime = 0
|
||||
|
||||
function SFrames:GetClassHexForName(name)
|
||||
if not name or name == "" then return nil end
|
||||
local cache = self.PlayerClassColorCache
|
||||
if cache[name] then return cache[name] end
|
||||
if self._classMissCache[name] then return nil end
|
||||
self:RefreshClassColorCache()
|
||||
return cache[name]
|
||||
if cache[name] then return cache[name] end
|
||||
self._classMissCache[name] = true
|
||||
local now = GetTime()
|
||||
if now - self._classMissCacheTime > 30 then
|
||||
self._classMissCacheTime = now
|
||||
self._classMissCache = {}
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function SFrames:GetLevelForName(name)
|
||||
@@ -730,27 +741,24 @@ end
|
||||
|
||||
local function ParseHardcoreDeathMessage(text)
|
||||
if type(text) ~= "string" or text == "" then return nil end
|
||||
if not string.find(text, "硬核") and not string.find(text, "死亡") then
|
||||
local lower = string.lower(text)
|
||||
if not string.find(lower, "hc news") and not string.find(lower, "has fallen")
|
||||
and not string.find(lower, "died") and not string.find(lower, "slain") then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
local clean = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
|
||||
clean = string.gsub(clean, "|r", "")
|
||||
|
||||
-- Check for Hardcore death signatures
|
||||
if string.find(string.lower(clean), "hc news") or string.find(clean, "硬核") or string.find(clean, "死亡") or string.find(string.lower(clean), "has fallen") or string.find(string.lower(clean), "died") or string.find(string.lower(clean), "slain") then
|
||||
-- Turtle English "Level 14"
|
||||
local _, _, lvlStr = string.find(clean, "Level%s+(%d+)")
|
||||
if lvlStr then return tonumber(lvlStr) end
|
||||
|
||||
-- Chinese "14级"
|
||||
local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级")
|
||||
if lvlStr2 then return tonumber(lvlStr2) end
|
||||
|
||||
-- Fallback
|
||||
local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)")
|
||||
if lvlStr3 then return tonumber(lvlStr3) end
|
||||
|
||||
-- If it matches death signatures but no level is found, return 1 as a baseline to trigger the filter.
|
||||
if string.find(string.lower(clean), "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(clean, "has fallen"))) then
|
||||
return 1
|
||||
end
|
||||
local _, _, lvlStr = string.find(clean, "Level%s+(%d+)")
|
||||
if lvlStr then return tonumber(lvlStr) end
|
||||
local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级")
|
||||
if lvlStr2 then return tonumber(lvlStr2) end
|
||||
local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)")
|
||||
if lvlStr3 then return tonumber(lvlStr3) end
|
||||
local lower = string.lower(clean)
|
||||
if string.find(lower, "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(lower, "has fallen"))) then
|
||||
return 1
|
||||
end
|
||||
return nil
|
||||
end
|
||||
@@ -1527,6 +1535,8 @@ local function EnsureDB()
|
||||
if type(db.editBoxPosition) ~= "string" then db.editBoxPosition = DEFAULTS.editBoxPosition end
|
||||
if type(db.editBoxX) ~= "number" then db.editBoxX = DEFAULTS.editBoxX end
|
||||
if type(db.editBoxY) ~= "number" then db.editBoxY = DEFAULTS.editBoxY end
|
||||
if db.translateEnabled == nil then db.translateEnabled = true end
|
||||
if db.chatMonitorEnabled == nil then db.chatMonitorEnabled = true end
|
||||
if type(db.layoutVersion) ~= "number" then db.layoutVersion = 1 end
|
||||
if db.layoutVersion < 2 then
|
||||
db.topPadding = DEFAULTS.topPadding
|
||||
@@ -3137,6 +3147,7 @@ function SFrames.Chat:ToggleConfigFrame()
|
||||
end
|
||||
|
||||
local CONFIG_PAGE_ORDER = {
|
||||
{ key = "general", label = "通用", title = "通用设置", desc = "翻译引擎和聊天消息监控的总开关。", icon = "settings" },
|
||||
{ key = "window", label = "窗口", title = "聊天窗口", desc = "尺寸、缩放、边框和输入框位置。", icon = "settings" },
|
||||
{ key = "tabs", label = "标签", title = "标签管理", desc = "切换、重命名、新建和删除聊天标签。", icon = "chat" },
|
||||
{ key = "filters", label = "过滤", title = "消息过滤", desc = "为当前标签设置消息类型和频道接收规则。", icon = "settings" },
|
||||
@@ -3442,6 +3453,57 @@ function SFrames.Chat:EnsureConfigFrame()
|
||||
return page
|
||||
end
|
||||
|
||||
local generalPage = CreatePage("general")
|
||||
do
|
||||
local engineSection = CreateCfgSection(generalPage, "翻译引擎", 0, 0, 584, 120, fontPath)
|
||||
|
||||
AddControl(CreateCfgCheck(engineSection, "启用 AI 翻译引擎", 16, -30,
|
||||
function() return EnsureDB().translateEnabled ~= false end,
|
||||
function(checked)
|
||||
EnsureDB().translateEnabled = (checked == true)
|
||||
end,
|
||||
function()
|
||||
SFrames.Chat:RefreshConfigFrame()
|
||||
end
|
||||
))
|
||||
|
||||
local transDesc = engineSection:CreateFontString(nil, "OVERLAY")
|
||||
transDesc:SetFont(fontPath, 10, "OUTLINE")
|
||||
transDesc:SetPoint("TOPLEFT", engineSection, "TOPLEFT", 38, -50)
|
||||
transDesc:SetWidth(520)
|
||||
transDesc:SetJustifyH("LEFT")
|
||||
transDesc:SetText("关闭后将完全停止调用 STranslateAPI 翻译接口,所有标签的自动翻译均不生效。")
|
||||
transDesc:SetTextColor(0.7, 0.7, 0.74)
|
||||
|
||||
local monitorSection = CreateCfgSection(generalPage, "聊天消息监控", 0, -136, 584, 160, fontPath)
|
||||
|
||||
AddControl(CreateCfgCheck(monitorSection, "启用聊天消息监控与收集", 16, -30,
|
||||
function() return EnsureDB().chatMonitorEnabled ~= false end,
|
||||
function(checked)
|
||||
EnsureDB().chatMonitorEnabled = (checked == true)
|
||||
end,
|
||||
function()
|
||||
SFrames.Chat:RefreshConfigFrame()
|
||||
end
|
||||
))
|
||||
|
||||
local monDesc = monitorSection:CreateFontString(nil, "OVERLAY")
|
||||
monDesc:SetFont(fontPath, 10, "OUTLINE")
|
||||
monDesc:SetPoint("TOPLEFT", monitorSection, "TOPLEFT", 38, -50)
|
||||
monDesc:SetWidth(520)
|
||||
monDesc:SetJustifyH("LEFT")
|
||||
monDesc:SetText("启用后将拦截聊天消息,提供消息历史缓存、右键复制 [+] 标记、频道翻译触发等功能。\n关闭后消息将原样通过,不做任何处理(翻译、复制等功能不可用)。")
|
||||
monDesc:SetTextColor(0.7, 0.7, 0.74)
|
||||
|
||||
local reloadHint = monitorSection:CreateFontString(nil, "OVERLAY")
|
||||
reloadHint:SetFont(fontPath, 10, "OUTLINE")
|
||||
reloadHint:SetPoint("TOPLEFT", monitorSection, "TOPLEFT", 38, -86)
|
||||
reloadHint:SetWidth(520)
|
||||
reloadHint:SetJustifyH("LEFT")
|
||||
reloadHint:SetText("提示:更改监控开关后建议 /reload 以确保完全生效。")
|
||||
reloadHint:SetTextColor(0.9, 0.75, 0.5)
|
||||
end
|
||||
|
||||
local windowPage = CreatePage("window")
|
||||
do
|
||||
local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 274, fontPath)
|
||||
@@ -6562,26 +6624,42 @@ function SFrames.Chat:Initialize()
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper: save a message to the persistent cache ring buffer
|
||||
local _persistWriteIdx = table.getn(db.messageCache)
|
||||
local function PersistMessage(msgID, text, r, g, b, frameIndex)
|
||||
local cache = EnsureDB().messageCache
|
||||
if not cache then
|
||||
EnsureDB().messageCache = {}
|
||||
cache = EnsureDB().messageCache
|
||||
end
|
||||
table.insert(cache, {
|
||||
id = msgID,
|
||||
text = text,
|
||||
r = r,
|
||||
g = g,
|
||||
b = b,
|
||||
frame = frameIndex or 1,
|
||||
time = date("%H:%M:%S"),
|
||||
})
|
||||
-- Trim to MAX_CACHE
|
||||
while table.getn(cache) > MAX_CACHE do
|
||||
table.remove(cache, 1)
|
||||
_persistWriteIdx = _persistWriteIdx + 1
|
||||
if _persistWriteIdx > MAX_CACHE then
|
||||
_persistWriteIdx = 1
|
||||
end
|
||||
local entry = cache[_persistWriteIdx]
|
||||
if entry then
|
||||
entry.id = msgID
|
||||
entry.text = text
|
||||
entry.r = r
|
||||
entry.g = g
|
||||
entry.b = b
|
||||
entry.frame = frameIndex or 1
|
||||
entry.time = date("%H:%M:%S")
|
||||
else
|
||||
cache[_persistWriteIdx] = {
|
||||
id = msgID,
|
||||
text = text,
|
||||
r = r,
|
||||
g = g,
|
||||
b = b,
|
||||
frame = frameIndex or 1,
|
||||
time = date("%H:%M:%S"),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local _cfTabCache = {}
|
||||
for i = 1, 7 do
|
||||
_cfTabCache[_G["ChatFrame" .. i]] = i
|
||||
end
|
||||
|
||||
for i = 1, 7 do
|
||||
@@ -6594,13 +6672,28 @@ function SFrames.Chat:Initialize()
|
||||
origAddMessage(self, text, r, g, b, alpha, holdTime)
|
||||
return
|
||||
end
|
||||
if string.sub(text, 1, 10) ~= "|Hsfchat:" and string.sub(text, 1, 12) ~= "|cff888888|H" then
|
||||
if EnsureDB().chatMonitorEnabled == false then
|
||||
origAddMessage(self, text, r, g, b, alpha, holdTime)
|
||||
return
|
||||
end
|
||||
local b1 = string.byte(text, 1)
|
||||
if b1 == 124 then
|
||||
local b2 = string.byte(text, 2)
|
||||
if b2 == 72 and string.find(text, "^|Hsfchat:") then
|
||||
origAddMessage(self, text, r, g, b, alpha, holdTime)
|
||||
return
|
||||
end
|
||||
if b2 == 99 and string.find(text, "^|cff888888|H") then
|
||||
origAddMessage(self, text, r, g, b, alpha, holdTime)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local db = EnsureDB()
|
||||
-- Universal catch for Turtle WoW custom chat channels like [硬核]
|
||||
local chanName = GetChannelNameFromChatLine(text)
|
||||
|
||||
if chanName and IsIgnoredChannelByDefault(chanName) then
|
||||
-- Global HC kill switch override check
|
||||
if db.hcGlobalDisable then
|
||||
local lowerChan = string.lower(chanName)
|
||||
if string.find(lowerChan, "hc") or string.find(chanName, "硬核") or string.find(lowerChan, "hardcore") then
|
||||
@@ -6608,46 +6701,32 @@ function SFrames.Chat:Initialize()
|
||||
end
|
||||
end
|
||||
|
||||
local frameName = self:GetName()
|
||||
local matchedTabIdx = nil
|
||||
if type(frameName) == "string" and string.find(frameName, "^ChatFrame%d+$") then
|
||||
matchedTabIdx = SFrames.Chat:GetTabIndexForChatFrame(self)
|
||||
if not matchedTabIdx then
|
||||
local _, _, frameNumStr = string.find(frameName, "^ChatFrame(%d+)$")
|
||||
local frameNum = tonumber(frameNumStr)
|
||||
if frameNum then
|
||||
local db = EnsureDB()
|
||||
if frameNum <= table.getn(db.tabs) then
|
||||
matchedTabIdx = frameNum
|
||||
end
|
||||
end
|
||||
end
|
||||
local matchedTabIdx = _cfTabCache[self]
|
||||
or SFrames.Chat:GetTabIndexForChatFrame(self)
|
||||
if not matchedTabIdx then
|
||||
return
|
||||
end
|
||||
if matchedTabIdx then
|
||||
local tab = EnsureDB().tabs[matchedTabIdx]
|
||||
if tab then
|
||||
if not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then
|
||||
return
|
||||
local tabs = db.tabs
|
||||
local tab = tabs and tabs[matchedTabIdx]
|
||||
if tab then
|
||||
if not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then
|
||||
return
|
||||
end
|
||||
local shouldTranslate = SFrames.Chat:GetTabChannelTranslateFilter(matchedTabIdx, chanName)
|
||||
if shouldTranslate then
|
||||
local cleanText = CleanTextForTranslation(text)
|
||||
cleanText = string.gsub(cleanText, "^%[.-%]%s*", "")
|
||||
local _, _, senderName = string.find(cleanText, "^%[([^%]]+)%]:%s*")
|
||||
if senderName then
|
||||
cleanText = string.gsub(cleanText, "^%[[^%]]+%]:%s*", "")
|
||||
end
|
||||
-- If it passed channel filter, allow translation for these bypassed channels
|
||||
local shouldTranslate = SFrames.Chat:GetTabChannelTranslateFilter(matchedTabIdx, chanName)
|
||||
if shouldTranslate then
|
||||
local cleanText = CleanTextForTranslation(text)
|
||||
-- Remove the channel prefix from translation text to be clean
|
||||
cleanText = string.gsub(cleanText, "^%[.-%]%s*", "")
|
||||
-- It might also have [PlayerName]:
|
||||
local _, _, senderName = string.find(cleanText, "^%[([^%]]+)%]:%s*")
|
||||
if senderName then
|
||||
cleanText = string.gsub(cleanText, "^%[[^%]]+%]:%s*", "")
|
||||
end
|
||||
if cleanText ~= "" then
|
||||
local tabId = tab.id
|
||||
SFrames.Chat:RequestAutoTranslation(cleanText, function(result)
|
||||
if result and result ~= "" then
|
||||
SFrames.Chat:AppendAutoTranslatedLine(tabId, "channel", chanName, cleanText, result, senderName)
|
||||
end
|
||||
end)
|
||||
end
|
||||
if cleanText ~= "" then
|
||||
local tabId = tab.id
|
||||
SFrames.Chat:RequestAutoTranslation(cleanText, function(result)
|
||||
if result and result ~= "" then
|
||||
SFrames.Chat:AppendAutoTranslatedLine(tabId, "channel", chanName, cleanText, result, senderName)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -6655,7 +6734,6 @@ function SFrames.Chat:Initialize()
|
||||
end
|
||||
end
|
||||
|
||||
-- Hardcore Death Event Overrides
|
||||
if db.hcDeathDisable or (db.hcDeathLevelMin and db.hcDeathLevelMin > 1) then
|
||||
local deathLvl = ParseHardcoreDeathMessage(text)
|
||||
if deathLvl then
|
||||
@@ -6667,47 +6745,13 @@ function SFrames.Chat:Initialize()
|
||||
SFrames.Chat.MessageIndex = SFrames.Chat.MessageIndex + 1
|
||||
local msgID = SFrames.Chat.MessageIndex
|
||||
|
||||
-- Store in runtime lookup
|
||||
SFrames.Chat.MessageHistory[msgID] = text
|
||||
|
||||
-- Persist to SavedVariables
|
||||
local frameIdx = nil
|
||||
for fi = 1, 7 do
|
||||
if self == _G["ChatFrame" .. fi] then
|
||||
frameIdx = fi
|
||||
break
|
||||
end
|
||||
end
|
||||
PersistMessage(msgID, text, r, g, b, frameIdx)
|
||||
PersistMessage(msgID, text, r, g, b, _cfTabCache[self])
|
||||
|
||||
-- Apply class color to player names in message
|
||||
local coloredText = ColorPlayerNamesInText(text)
|
||||
-- Insert the clickable button [+] at the beginning
|
||||
local modifiedText = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. coloredText
|
||||
origAddMessage(self, modifiedText, r, g, b, alpha, holdTime)
|
||||
|
||||
-- Legacy auto-translate disabled; handled by per-tab routing above.
|
||||
if false then
|
||||
if self == ChatFrame1 and not string.find(text, "%[翻译%]") then
|
||||
if string.find(text, "%[.-硬核.-%]") or string.find(string.lower(text), "%[.-hc.-%]") or string.find(string.lower(text), "%[.-hardcore.-%]") then
|
||||
local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
|
||||
cleanText = string.gsub(cleanText, "|r", "")
|
||||
cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1")
|
||||
|
||||
if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then
|
||||
_G.STranslateAPI.Translate(cleanText, "auto", "zh", function(result, err, meta)
|
||||
if result then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[翻译] |cffffff00" .. tostring(result) .. "|r")
|
||||
end
|
||||
end, "Nanami-UI")
|
||||
elseif _G.STranslate and _G.STranslate.SendIO then
|
||||
_G.STranslate:SendIO(cleanText, "IN", "auto", "zh")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
origAddMessage(self, text, r, g, b, alpha, holdTime)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
10
ConfigUI.lua
10
ConfigUI.lua
@@ -509,7 +509,6 @@ local function EnsureDB()
|
||||
|
||||
if type(SFramesDB.Tweaks) ~= "table" then SFramesDB.Tweaks = {} end
|
||||
if SFramesDB.Tweaks.autoStance == nil then SFramesDB.Tweaks.autoStance = true end
|
||||
if SFramesDB.Tweaks.autoDismount == nil then SFramesDB.Tweaks.autoDismount = true end
|
||||
if SFramesDB.Tweaks.superWoW == nil then SFramesDB.Tweaks.superWoW = true end
|
||||
if SFramesDB.Tweaks.turtleCompat == nil then SFramesDB.Tweaks.turtleCompat = true end
|
||||
if SFramesDB.Tweaks.cooldownNumbers == nil then SFramesDB.Tweaks.cooldownNumbers = true end
|
||||
@@ -1090,14 +1089,7 @@ function SFrames.ConfigUI:BuildUIPage()
|
||||
CreateDesc(tweaksSection, "施法需要特定姿态时自动切换(如人形按熊掌→自动变熊)", 36, -50, font, 218)
|
||||
|
||||
table.insert(controls, CreateCheckBox(tweaksSection,
|
||||
"自动取消变形/下马", 270, -34,
|
||||
function() return SFramesDB.Tweaks.autoDismount ~= false end,
|
||||
function(checked) SFramesDB.Tweaks.autoDismount = checked end
|
||||
))
|
||||
CreateDesc(tweaksSection, "变形/骑马时施法自动取消(如熊形按治疗→自动回人形)", 292, -50, font, 218)
|
||||
|
||||
table.insert(controls, CreateCheckBox(tweaksSection,
|
||||
"乌龟服兼容修改", 14, -80,
|
||||
"乌龟服兼容修改", 270, -34,
|
||||
function() return SFramesDB.Tweaks.turtleCompat ~= false end,
|
||||
function(checked) SFramesDB.Tweaks.turtleCompat = checked end
|
||||
))
|
||||
|
||||
12
Core.lua
12
Core.lua
@@ -83,9 +83,11 @@ end
|
||||
|
||||
-- Event Dispatcher
|
||||
SFrames.eventFrame:SetScript("OnEvent", function()
|
||||
if SFrames.events[event] then
|
||||
for i, func in ipairs(SFrames.events[event]) do
|
||||
func(event)
|
||||
local handlers = SFrames.events[event]
|
||||
if handlers then
|
||||
local n = table.getn(handlers)
|
||||
for i = 1, n do
|
||||
handlers[i](event)
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -100,8 +102,8 @@ end
|
||||
|
||||
function SFrames:UnregisterEvent(event, func)
|
||||
if self.events[event] then
|
||||
for i, f in ipairs(self.events[event]) do
|
||||
if f == func then
|
||||
for i = 1, table.getn(self.events[event]) do
|
||||
if self.events[event][i] == func then
|
||||
table.remove(self.events[event], i)
|
||||
break
|
||||
end
|
||||
|
||||
@@ -147,7 +147,11 @@ function SFrames.FloatingTooltip:Initialize()
|
||||
bg:SetAllPoints(bgFrame)
|
||||
GameTooltip._nanamiBGTex = bg
|
||||
|
||||
bgFrame._ttVisCheck = 0
|
||||
bgFrame:SetScript("OnUpdate", function()
|
||||
this._ttVisCheck = this._ttVisCheck + arg1
|
||||
if this._ttVisCheck < 0.2 then return end
|
||||
this._ttVisCheck = 0
|
||||
if not GameTooltip:IsVisible() then
|
||||
this:Hide()
|
||||
end
|
||||
|
||||
@@ -812,10 +812,20 @@ local function UpdateFilters()
|
||||
MainFrame.filterUsed:SetActive(currentFilter == "used")
|
||||
end
|
||||
|
||||
local _isUpdating = false
|
||||
|
||||
local function FullUpdate()
|
||||
UpdateFilters()
|
||||
UpdateList()
|
||||
UpdateDetail()
|
||||
if _isUpdating then return end
|
||||
_isUpdating = true
|
||||
local ok, err = pcall(function()
|
||||
UpdateFilters()
|
||||
UpdateList()
|
||||
UpdateDetail()
|
||||
end)
|
||||
_isUpdating = false
|
||||
if not ok then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI TrainerUI: FullUpdate error: " .. tostring(err) .. "|r")
|
||||
end
|
||||
end
|
||||
|
||||
local function SelectService(index)
|
||||
@@ -1181,12 +1191,6 @@ function TUI:Initialize()
|
||||
ClassTrainerFrame:EnableMouse(false)
|
||||
end
|
||||
|
||||
if SetTrainerServiceTypeFilter then
|
||||
SetTrainerServiceTypeFilter("available", 1)
|
||||
SetTrainerServiceTypeFilter("unavailable", 1)
|
||||
SetTrainerServiceTypeFilter("used", 1)
|
||||
end
|
||||
|
||||
selectedIndex = nil
|
||||
currentFilter = "all"
|
||||
collapsedCats = {}
|
||||
@@ -1196,6 +1200,15 @@ function TUI:Initialize()
|
||||
npcName = npcName .. " - 专业训练"
|
||||
end
|
||||
MainFrame.npcNameFS:SetText(npcName)
|
||||
|
||||
_isUpdating = true
|
||||
if SetTrainerServiceTypeFilter then
|
||||
pcall(SetTrainerServiceTypeFilter, "available", 1)
|
||||
pcall(SetTrainerServiceTypeFilter, "unavailable", 1)
|
||||
pcall(SetTrainerServiceTypeFilter, "used", 1)
|
||||
end
|
||||
_isUpdating = false
|
||||
|
||||
MainFrame:Show()
|
||||
BuildDisplayList()
|
||||
for _, entry in ipairs(displayList) do
|
||||
|
||||
209
Tweaks.lua
209
Tweaks.lua
@@ -6,8 +6,7 @@
|
||||
-- 4. Cooldown Numbers - show remaining cooldown time as text overlay
|
||||
-- 5. Dark UI - darken the entire interface
|
||||
-- 6. WorldMap Window - turn fullscreen map into a movable/scalable window
|
||||
-- 7. Auto Dismount - cancel shapeshift/mount when casting incompatible spells
|
||||
-- 8. Hunter Aspect Guard - auto switch to Hawk when taking damage with Cheetah/Pack
|
||||
-- 7. Hunter Aspect Guard - cancel Cheetah/Pack when taking damage in combat (avoid OOC false positives)
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
SFrames.Tweaks = SFrames.Tweaks or {}
|
||||
@@ -75,120 +74,6 @@ local function InitAutoStance()
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Auto Dismount / Cancel Shapeshift
|
||||
-- When casting a spell that fails because you are mounted or shapeshifted,
|
||||
-- automatically cancel the mount/shapeshift buff so the next cast succeeds.
|
||||
--------------------------------------------------------------------------------
|
||||
local function InitAutoDismount()
|
||||
local dismount = CreateFrame("Frame", "NanamiAutoDismount")
|
||||
local _, playerClass = UnitClass("player")
|
||||
|
||||
local scanner = CreateFrame("GameTooltip", "NanamiDismountScan", nil, "GameTooltipTemplate")
|
||||
scanner:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||||
scanner:SetAlpha(0)
|
||||
scanner:Hide()
|
||||
|
||||
local mountStrings = {
|
||||
"^Increases speed by (.+)%%",
|
||||
"^Erhöht Tempo um (.+)%%",
|
||||
"^Aumenta la velocidad en un (.+)%%",
|
||||
"^Augmente la vitesse de (.+)%%",
|
||||
"^Скорость увеличена на (.+)%%",
|
||||
"^이동 속도 (.+)%%만큼 증가",
|
||||
"^速度提高(.+)%%", "^移动速度提高(.+)%%",
|
||||
"speed based on", "Slow and steady...", "Riding",
|
||||
"Lento y constante...", "Aumenta la velocidad según tu habilidad de Montar.",
|
||||
"根据您的骑行技能提高速度。", "根据骑术技能提高速度。", "又慢又稳......",
|
||||
}
|
||||
|
||||
local shapeshiftIcons = {
|
||||
"ability_racial_bearform", "ability_druid_catform",
|
||||
"ability_druid_travelform", "spell_nature_forceofnature",
|
||||
"ability_druid_aquaticform", "spell_nature_spiritwolf",
|
||||
"ability_druid_treeoflife", "ability_druid_stagform",
|
||||
}
|
||||
|
||||
local hunterAspectIcons = {
|
||||
"ability_mount_jungletiger",
|
||||
"ability_mount_packhorse",
|
||||
}
|
||||
|
||||
local errorStrings = {}
|
||||
local errorGlobals = {
|
||||
"SPELL_FAILED_NOT_MOUNTED", "ERR_ATTACK_MOUNTED", "ERR_TAXIPLAYERALREADYMOUNTED",
|
||||
"ERR_NOT_WHILE_MOUNTED",
|
||||
"SPELL_FAILED_NOT_SHAPESHIFT", "SPELL_FAILED_NO_ITEMS_WHILE_SHAPESHIFTED",
|
||||
"SPELL_NOT_SHAPESHIFTED", "SPELL_NOT_SHAPESHIFTED_NOSPACE",
|
||||
"ERR_CANT_INTERACT_SHAPESHIFTED", "ERR_NOT_WHILE_SHAPESHIFTED",
|
||||
"ERR_NO_ITEMS_WHILE_SHAPESHIFTED", "ERR_TAXIPLAYERSHAPESHIFTED",
|
||||
"ERR_MOUNT_SHAPESHIFTED",
|
||||
}
|
||||
for _, name in pairs(errorGlobals) do
|
||||
local val = getfenv(0)[name]
|
||||
if val then table.insert(errorStrings, val) end
|
||||
end
|
||||
|
||||
dismount:RegisterEvent("UI_ERROR_MESSAGE")
|
||||
dismount:SetScript("OnEvent", function()
|
||||
if arg1 == SPELL_FAILED_NOT_STANDING then
|
||||
SitOrStand()
|
||||
return
|
||||
end
|
||||
|
||||
local matched = false
|
||||
for _, err in pairs(errorStrings) do
|
||||
if arg1 == err then matched = true break end
|
||||
end
|
||||
if not matched then return end
|
||||
|
||||
for i = 0, 31 do
|
||||
local buff = GetPlayerBuffTexture(i)
|
||||
if buff then
|
||||
local lowerBuff = string.lower(buff)
|
||||
|
||||
local skip = false
|
||||
if playerClass == "HUNTER" then
|
||||
for _, tex in pairs(hunterAspectIcons) do
|
||||
if string.find(lowerBuff, tex) then
|
||||
skip = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not skip then
|
||||
scanner:ClearLines()
|
||||
scanner:SetPlayerBuff(i)
|
||||
for line = 1, scanner:NumLines() do
|
||||
local text = getfenv(0)["NanamiDismountScanTextLeft" .. line]
|
||||
if text and text:GetText() then
|
||||
for _, str in pairs(mountStrings) do
|
||||
if string.find(text:GetText(), str) then
|
||||
CancelPlayerBuff(i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, icon in pairs(shapeshiftIcons) do
|
||||
if string.find(lowerBuff, icon) then
|
||||
CancelPlayerBuff(i)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if string.find(lowerBuff, "ability_mount_") then
|
||||
CancelPlayerBuff(i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- SuperWoW Compatibility
|
||||
-- Provides GUID-based cast/channel data when SuperWoW client mod is active.
|
||||
@@ -389,6 +274,78 @@ local function TimeConvert(remaining)
|
||||
end
|
||||
end
|
||||
|
||||
local _activeCooldowns = {}
|
||||
local _cdTickTimer = 0
|
||||
local _cdUpdaterFrame
|
||||
|
||||
local function CooldownSharedUpdate()
|
||||
_cdTickTimer = _cdTickTimer + arg1
|
||||
if _cdTickTimer < 0.1 then return end
|
||||
_cdTickTimer = 0
|
||||
|
||||
local now = GetTime()
|
||||
local sysTime = time()
|
||||
|
||||
local n = table.getn(_activeCooldowns)
|
||||
local i = 1
|
||||
while i <= n do
|
||||
local cdFrame = _activeCooldowns[i]
|
||||
if cdFrame and cdFrame:IsShown() then
|
||||
local parent = cdFrame:GetParent()
|
||||
if parent then
|
||||
cdFrame:SetAlpha(parent:GetAlpha())
|
||||
end
|
||||
if cdFrame.start < now then
|
||||
local remaining = cdFrame.duration - (now - cdFrame.start)
|
||||
if remaining > 0 then
|
||||
cdFrame.text:SetText(TimeConvert(remaining))
|
||||
else
|
||||
cdFrame:Hide()
|
||||
_activeCooldowns[i] = _activeCooldowns[n]
|
||||
_activeCooldowns[n] = nil
|
||||
n = n - 1
|
||||
i = i - 1
|
||||
end
|
||||
else
|
||||
local startupTime = sysTime - now
|
||||
local cdTime = (2 ^ 32) / 1000 - cdFrame.start
|
||||
local cdStartTime = startupTime - cdTime
|
||||
local cdEndTime = cdStartTime + cdFrame.duration
|
||||
local remaining = cdEndTime - sysTime
|
||||
if remaining >= 0 then
|
||||
cdFrame.text:SetText(TimeConvert(remaining))
|
||||
else
|
||||
cdFrame:Hide()
|
||||
_activeCooldowns[i] = _activeCooldowns[n]
|
||||
_activeCooldowns[n] = nil
|
||||
n = n - 1
|
||||
i = i - 1
|
||||
end
|
||||
end
|
||||
i = i + 1
|
||||
else
|
||||
_activeCooldowns[i] = _activeCooldowns[n]
|
||||
_activeCooldowns[n] = nil
|
||||
n = n - 1
|
||||
end
|
||||
end
|
||||
if n == 0 and _cdUpdaterFrame then
|
||||
_cdUpdaterFrame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local function RegisterCooldownFrame(cdFrame)
|
||||
for i = 1, table.getn(_activeCooldowns) do
|
||||
if _activeCooldowns[i] == cdFrame then return end
|
||||
end
|
||||
table.insert(_activeCooldowns, cdFrame)
|
||||
if not _cdUpdaterFrame then
|
||||
_cdUpdaterFrame = CreateFrame("Frame", "NanamiCDSharedUpdater", UIParent)
|
||||
_cdUpdaterFrame:SetScript("OnUpdate", CooldownSharedUpdate)
|
||||
end
|
||||
_cdUpdaterFrame:Show()
|
||||
end
|
||||
|
||||
local function CooldownOnUpdate()
|
||||
local parent = this:GetParent()
|
||||
if not parent then this:Hide() return end
|
||||
@@ -494,7 +451,7 @@ local function CreateCoolDown(cooldown, start, duration)
|
||||
cooldown.cooldowntext.text:SetPoint("CENTER", cooldown.cooldowntext, "CENTER", 0, 0)
|
||||
end
|
||||
|
||||
cooldown.cooldowntext:SetScript("OnUpdate", CooldownOnUpdate)
|
||||
RegisterCooldownFrame(cooldown.cooldowntext)
|
||||
end
|
||||
|
||||
local function SetCooldown(frame, start, duration, enable)
|
||||
@@ -520,12 +477,13 @@ local function SetCooldown(frame, start, duration, enable)
|
||||
if start > 0 and duration > 0 and (not enable or enable > 0) then
|
||||
if frame.cooldownmask then frame.cooldownmask:Show() end
|
||||
frame.cooldowntext:Show()
|
||||
frame.cooldowntext.start = start
|
||||
frame.cooldowntext.duration = duration
|
||||
RegisterCooldownFrame(frame.cooldowntext)
|
||||
else
|
||||
if frame.cooldownmask then frame.cooldownmask:Hide() end
|
||||
frame.cooldowntext:Hide()
|
||||
end
|
||||
frame.cooldowntext.start = start
|
||||
frame.cooldowntext.duration = duration
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1061,8 +1019,8 @@ end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Hunter Aspect Guard
|
||||
-- When a Hunter takes damage with Aspect of the Cheetah or Aspect of the Pack
|
||||
-- active, automatically cancel the aspect to prevent repeated dazing.
|
||||
-- When a Hunter takes damage in combat with Aspect of the Cheetah or Pack
|
||||
-- active, cancel the aspect to reduce daze chains. OOC HP changes are ignored.
|
||||
--------------------------------------------------------------------------------
|
||||
local function InitHunterAspectGuard()
|
||||
local _, playerClass = UnitClass("player")
|
||||
@@ -1104,7 +1062,7 @@ local function InitHunterAspectGuard()
|
||||
return
|
||||
end
|
||||
|
||||
if lastHP > 0 and hp < lastHP then
|
||||
if lastHP > 0 and hp < lastHP and UnitAffectingCombat("player") then
|
||||
if GetTime() - lastCancel >= 1.0 then
|
||||
if CancelDangerousAspect() then
|
||||
lastCancel = GetTime()
|
||||
@@ -1129,13 +1087,6 @@ function Tweaks:Initialize()
|
||||
end
|
||||
end
|
||||
|
||||
if cfg.autoDismount ~= false then
|
||||
local ok, err = pcall(InitAutoDismount)
|
||||
if not ok then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: AutoDismount init failed: " .. tostring(err) .. "|r")
|
||||
end
|
||||
end
|
||||
|
||||
if cfg.superWoW ~= false then
|
||||
local ok, err = pcall(InitSuperWoW)
|
||||
if not ok then
|
||||
|
||||
@@ -493,7 +493,7 @@ function SFrames.Party:Initialize()
|
||||
|
||||
if not self._globalUpdateFrame then
|
||||
self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent)
|
||||
self._globalUpdateFrame:SetScript("OnUpdate", function()
|
||||
self._globalUpdateFrame._onUpdateFunc = function()
|
||||
if SFrames.Party.testing then return end
|
||||
local dt = arg1
|
||||
local frames = SFrames.Party.frames
|
||||
@@ -533,7 +533,8 @@ function SFrames.Party:Initialize()
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
self._globalUpdateFrame:Hide()
|
||||
end
|
||||
|
||||
self:ApplyConfig()
|
||||
@@ -745,20 +746,35 @@ function SFrames.Party:UpdateAll()
|
||||
self.frames[i].frame:Hide()
|
||||
end
|
||||
end
|
||||
if self._globalUpdateFrame then
|
||||
self._globalUpdateFrame:SetScript("OnUpdate", nil)
|
||||
self._globalUpdateFrame:Hide()
|
||||
end
|
||||
return
|
||||
end
|
||||
if self.testing then return end
|
||||
local numParty = GetNumPartyMembers()
|
||||
local hasVisible = false
|
||||
for i = 1, 4 do
|
||||
local data = self.frames[i]
|
||||
local f = data.frame
|
||||
if i <= numParty then
|
||||
f:Show()
|
||||
self:UpdateFrame(data.unit)
|
||||
hasVisible = true
|
||||
else
|
||||
f:Hide()
|
||||
end
|
||||
end
|
||||
if self._globalUpdateFrame then
|
||||
if hasVisible then
|
||||
self._globalUpdateFrame:SetScript("OnUpdate", self._globalUpdateFrame._onUpdateFunc)
|
||||
self._globalUpdateFrame:Show()
|
||||
else
|
||||
self._globalUpdateFrame:SetScript("OnUpdate", nil)
|
||||
self._globalUpdateFrame:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SFrames.Party:UpdateFrame(unit)
|
||||
|
||||
@@ -539,11 +539,13 @@ function SFrames.Raid:EnsureFrames()
|
||||
|
||||
if not self._globalUpdateFrame then
|
||||
self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent)
|
||||
self._globalUpdateFrame:SetScript("OnUpdate", function()
|
||||
self._globalUpdateFrame._onUpdateFunc = function()
|
||||
if SFrames.Raid.testing then return end
|
||||
local dt = arg1
|
||||
local frames = SFrames.Raid.frames
|
||||
if not frames then return end
|
||||
local visibleCount = SFrames.Raid._visibleCount or 0
|
||||
if visibleCount == 0 then return end
|
||||
for i = 1, 40 do
|
||||
local entry = frames[i]
|
||||
if entry then
|
||||
@@ -573,7 +575,8 @@ function SFrames.Raid:EnsureFrames()
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
self._globalUpdateFrame:Hide()
|
||||
end
|
||||
|
||||
self:ApplyLayout()
|
||||
@@ -602,6 +605,7 @@ function SFrames.Raid:UpdateAll()
|
||||
groupSlots[g] = 0
|
||||
end
|
||||
|
||||
local visibleCount = 0
|
||||
for i = 1, 40 do
|
||||
local name, rank, subgroup = GetRaidRosterInfo(i)
|
||||
if name and subgroup and subgroup >= 1 and subgroup <= 8 then
|
||||
@@ -615,9 +619,16 @@ function SFrames.Raid:UpdateAll()
|
||||
self.frames[frameIndex].unit = "raid" .. i
|
||||
f:Show()
|
||||
self:UpdateFrame("raid" .. i)
|
||||
visibleCount = visibleCount + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
self._visibleCount = visibleCount
|
||||
|
||||
if self._globalUpdateFrame then
|
||||
self._globalUpdateFrame:SetScript("OnUpdate", self._globalUpdateFrame._onUpdateFunc)
|
||||
self._globalUpdateFrame:Show()
|
||||
end
|
||||
|
||||
-- Show/hide group labels based on whether each group has members
|
||||
local showLabel = SFramesDB and SFramesDB.raidShowGroupLabel ~= false
|
||||
@@ -632,6 +643,11 @@ function SFrames.Raid:UpdateAll()
|
||||
end
|
||||
end
|
||||
else
|
||||
self._visibleCount = 0
|
||||
if self._globalUpdateFrame then
|
||||
self._globalUpdateFrame:SetScript("OnUpdate", nil)
|
||||
self._globalUpdateFrame:Hide()
|
||||
end
|
||||
if not self.testing and self._framesBuilt then
|
||||
for i = 1, 40 do
|
||||
self.frames[i].frame:Hide()
|
||||
|
||||
89
docs/AutoDismount-技术总结.md
Normal file
89
docs/AutoDismount-技术总结.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 自动下马 / 取消变形(Auto Dismount)技术总结
|
||||
|
||||
## 功能概述
|
||||
|
||||
骑乘或变形状态下尝试施法时,自动取消坐骑 / 变形 buff,使下次施法直接生效。
|
||||
|
||||
## 检测架构(三层)
|
||||
|
||||
### 第 1 层 — 错误消息匹配
|
||||
|
||||
监听 `UI_ERROR_MESSAGE` 事件:
|
||||
|
||||
| 方式 | 说明 |
|
||||
|---|---|
|
||||
| 哈希表精确匹配 | `ERR_ATTACK_MOUNTED`, `ERR_NOT_WHILE_MOUNTED`, `ERR_MOUNT_SHAPESHIFTED` 等标准全局变量 |
|
||||
| 关键词模糊匹配 | `mounted`, `shapeshifted`, `坐骑`, `骑乘`, `变形`, `形态`(兜底自定义错误消息) |
|
||||
|
||||
> Turtle WoW 使用自定义错误 `"你正在骑乘状态"`,不在标准全局变量中,通过关键词 `"骑乘"` 命中。
|
||||
|
||||
### 第 2 层 — Dismount() API
|
||||
|
||||
客户端若提供 `Dismount()` 函数则先调用(`pcall` 包裹),**不 early return**,继续 buff 扫描作为后备。
|
||||
|
||||
### 第 3 层 — Buff 扫描(0-39)
|
||||
|
||||
遍历玩家 buff,逐个按优先级检测:
|
||||
|
||||
| 优先级 | 检测方式 | 目标 | 示例 |
|
||||
|---|---|---|---|
|
||||
| 1 | 变形图标匹配 | 德鲁伊 / 萨满形态 | `ability_druid_catform`, `spell_nature_spiritwolf` |
|
||||
| 2 | 坐骑图标模式 | 坐骑 buff 图标 | `ability_mount_*`, `inv_pet_speedy` |
|
||||
| 3 | Tooltip 速度文本 | 坐骑速度描述(多语言) | `"又慢又稳"`, `"Increases speed by"` |
|
||||
| 4 | Buff 名称关键词 | Tooltip 首行含坐骑关键词 | `"骑乘"`, `"riding"` |
|
||||
|
||||
### 猎人守护跳过
|
||||
|
||||
猎人的猎豹 / 豹群守护使用 `ability_mount_*` 图标,需特殊跳过以免误取消。
|
||||
|
||||
## 调试关键发现
|
||||
|
||||
### Tooltip 扫描在 Turtle WoW 中失效
|
||||
|
||||
`GameTooltip:SetPlayerBuff(index)` 对所有 buff 均返回 **0 行**。
|
||||
即使重新 `SetOwner`、尝试 `GetPlayerBuff` 返回的 buffId,结果相同。
|
||||
|
||||
**影响**:tooltip 检测路径完全失效,必须依赖图标模式匹配。
|
||||
|
||||
### 非标准坐骑图标
|
||||
|
||||
| 坐骑 | 实际图标纹理 |
|
||||
|---|---|
|
||||
| 骑乘乌龟 | `inv_pet_speedy`(不匹配 `ability_mount_*`) |
|
||||
| 标准坐骑 | `Ability_Mount_*` |
|
||||
| AQ 坐骑 | `inv_misc_qirajicrystal_*` |
|
||||
|
||||
## 完整图标模式清单
|
||||
|
||||
```lua
|
||||
-- 坐骑
|
||||
mountIconPatterns = {
|
||||
"ability_mount_",
|
||||
"inv_misc_qirajicrystal",
|
||||
"inv_pet_speedy",
|
||||
}
|
||||
-- 变形
|
||||
shapeshiftIcons = {
|
||||
"ability_racial_bearform", -- 熊形态
|
||||
"ability_druid_catform", -- 猫形态
|
||||
"ability_druid_travelform", -- 旅行形态
|
||||
"spell_nature_forceofnature", -- 枭兽形态
|
||||
"ability_druid_aquaticform", -- 水栖形态
|
||||
"spell_nature_spiritwolf", -- 幽魂之狼
|
||||
"ability_druid_treeoflife", -- 生命之树
|
||||
"ability_druid_stagform", -- 鹿形态
|
||||
"ability_druid_flightform", -- 飞行形态
|
||||
}
|
||||
-- 猎人守护(跳过)
|
||||
hunterAspectIcons = {
|
||||
"ability_mount_jungletiger", -- 猎豹守护
|
||||
"ability_mount_packhorse", -- 豹群守护
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展方法
|
||||
|
||||
遇到新坐骑无法自动下马时:
|
||||
1. 在 `InitAutoDismount` 中将 `_debug` 设为 `true`
|
||||
2. 骑上坐骑施法,查看聊天窗口中的 `[Nanami-DBG] buff[x]=` 输出
|
||||
3. 将新坐骑的图标关键字添加到 `mountIconPatterns`
|
||||
Reference in New Issue
Block a user