修复猎人野兽训练展示技能所需训练点数错误问题

修复拾取问题
修复其他已知问题
修复自动下马问题以及带来的猎人守护自动关闭问题
彻底修复拾取界面问题
修复目标框架的施法条监控
修复其他已知问题
调整dps插件对仇恨的估算方式
优化dps插件
修复远程攻击条问题
This commit is contained in:
rucky
2026-03-25 00:56:49 +08:00
parent c0f1ecc713
commit c7dd0f4848
9 changed files with 393 additions and 266 deletions

264
Chat.lua
View File

@@ -374,12 +374,23 @@ function SFrames:RefreshClassColorCache()
PersistClassCache() PersistClassCache()
end end
SFrames._classMissCache = {}
SFrames._classMissCacheTime = 0
function SFrames:GetClassHexForName(name) function SFrames:GetClassHexForName(name)
if not name or name == "" then return nil end if not name or name == "" then return nil end
local cache = self.PlayerClassColorCache local cache = self.PlayerClassColorCache
if cache[name] then return cache[name] end if cache[name] then return cache[name] end
if self._classMissCache[name] then return nil end
self:RefreshClassColorCache() 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 end
function SFrames:GetLevelForName(name) function SFrames:GetLevelForName(name)
@@ -730,27 +741,24 @@ end
local function ParseHardcoreDeathMessage(text) local function ParseHardcoreDeathMessage(text)
if type(text) ~= "string" or text == "" then return nil end 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", "") local clean = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
clean = string.gsub(clean, "|r", "") clean = string.gsub(clean, "|r", "")
local _, _, lvlStr = string.find(clean, "Level%s+(%d+)")
-- Check for Hardcore death signatures if lvlStr then return tonumber(lvlStr) end
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 local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级")
-- Turtle English "Level 14" if lvlStr2 then return tonumber(lvlStr2) end
local _, _, lvlStr = string.find(clean, "Level%s+(%d+)") local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)")
if lvlStr then return tonumber(lvlStr) end if lvlStr3 then return tonumber(lvlStr3) end
local lower = string.lower(clean)
-- Chinese "14级" if string.find(lower, "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(lower, "has fallen"))) then
local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级") return 1
if lvlStr2 then return tonumber(lvlStr2) end
-- Fallback
local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)")
if lvlStr3 then return tonumber(lvlStr3) end
-- If it matches death signatures but no level is found, return 1 as a baseline to trigger the filter.
if string.find(string.lower(clean), "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(clean, "has fallen"))) then
return 1
end
end end
return nil return nil
end end
@@ -1527,6 +1535,8 @@ local function EnsureDB()
if type(db.editBoxPosition) ~= "string" then db.editBoxPosition = DEFAULTS.editBoxPosition end if type(db.editBoxPosition) ~= "string" then db.editBoxPosition = DEFAULTS.editBoxPosition end
if type(db.editBoxX) ~= "number" then db.editBoxX = DEFAULTS.editBoxX end if type(db.editBoxX) ~= "number" then db.editBoxX = DEFAULTS.editBoxX end
if type(db.editBoxY) ~= "number" then db.editBoxY = DEFAULTS.editBoxY 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 type(db.layoutVersion) ~= "number" then db.layoutVersion = 1 end
if db.layoutVersion < 2 then if db.layoutVersion < 2 then
db.topPadding = DEFAULTS.topPadding db.topPadding = DEFAULTS.topPadding
@@ -3137,6 +3147,7 @@ function SFrames.Chat:ToggleConfigFrame()
end end
local CONFIG_PAGE_ORDER = { local CONFIG_PAGE_ORDER = {
{ key = "general", label = "通用", title = "通用设置", desc = "翻译引擎和聊天消息监控的总开关。", icon = "settings" },
{ key = "window", label = "窗口", title = "聊天窗口", desc = "尺寸、缩放、边框和输入框位置。", icon = "settings" }, { key = "window", label = "窗口", title = "聊天窗口", desc = "尺寸、缩放、边框和输入框位置。", icon = "settings" },
{ key = "tabs", label = "标签", title = "标签管理", desc = "切换、重命名、新建和删除聊天标签。", icon = "chat" }, { key = "tabs", label = "标签", title = "标签管理", desc = "切换、重命名、新建和删除聊天标签。", icon = "chat" },
{ key = "filters", label = "过滤", title = "消息过滤", desc = "为当前标签设置消息类型和频道接收规则。", icon = "settings" }, { key = "filters", label = "过滤", title = "消息过滤", desc = "为当前标签设置消息类型和频道接收规则。", icon = "settings" },
@@ -3442,6 +3453,57 @@ function SFrames.Chat:EnsureConfigFrame()
return page return page
end 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") local windowPage = CreatePage("window")
do do
local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 274, fontPath) local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 274, fontPath)
@@ -6562,26 +6624,42 @@ function SFrames.Chat:Initialize()
end end
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 function PersistMessage(msgID, text, r, g, b, frameIndex)
local cache = EnsureDB().messageCache local cache = EnsureDB().messageCache
if not cache then if not cache then
EnsureDB().messageCache = {} EnsureDB().messageCache = {}
cache = EnsureDB().messageCache cache = EnsureDB().messageCache
end end
table.insert(cache, { _persistWriteIdx = _persistWriteIdx + 1
id = msgID, if _persistWriteIdx > MAX_CACHE then
text = text, _persistWriteIdx = 1
r = r,
g = g,
b = b,
frame = frameIndex or 1,
time = date("%H:%M:%S"),
})
-- Trim to MAX_CACHE
while table.getn(cache) > MAX_CACHE do
table.remove(cache, 1)
end end
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 end
for i = 1, 7 do for i = 1, 7 do
@@ -6594,13 +6672,28 @@ function SFrames.Chat:Initialize()
origAddMessage(self, text, r, g, b, alpha, holdTime) origAddMessage(self, text, r, g, b, alpha, holdTime)
return return
end 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() local db = EnsureDB()
-- Universal catch for Turtle WoW custom chat channels like [硬核]
local chanName = GetChannelNameFromChatLine(text) local chanName = GetChannelNameFromChatLine(text)
if chanName and IsIgnoredChannelByDefault(chanName) then if chanName and IsIgnoredChannelByDefault(chanName) then
-- Global HC kill switch override check
if db.hcGlobalDisable then if db.hcGlobalDisable then
local lowerChan = string.lower(chanName) local lowerChan = string.lower(chanName)
if string.find(lowerChan, "hc") or string.find(chanName, "硬核") or string.find(lowerChan, "hardcore") then 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
end end
local frameName = self:GetName() local matchedTabIdx = _cfTabCache[self]
local matchedTabIdx = nil or SFrames.Chat:GetTabIndexForChatFrame(self)
if type(frameName) == "string" and string.find(frameName, "^ChatFrame%d+$") then if not matchedTabIdx then
matchedTabIdx = SFrames.Chat:GetTabIndexForChatFrame(self) return
if not matchedTabIdx then
local _, _, frameNumStr = string.find(frameName, "^ChatFrame(%d+)$")
local frameNum = tonumber(frameNumStr)
if frameNum then
local db = EnsureDB()
if frameNum <= table.getn(db.tabs) then
matchedTabIdx = frameNum
end
end
end
end end
if matchedTabIdx then local tabs = db.tabs
local tab = EnsureDB().tabs[matchedTabIdx] local tab = tabs and tabs[matchedTabIdx]
if tab then if tab then
if not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then if not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then
return 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 end
-- If it passed channel filter, allow translation for these bypassed channels if cleanText ~= "" then
local shouldTranslate = SFrames.Chat:GetTabChannelTranslateFilter(matchedTabIdx, chanName) local tabId = tab.id
if shouldTranslate then SFrames.Chat:RequestAutoTranslation(cleanText, function(result)
local cleanText = CleanTextForTranslation(text) if result and result ~= "" then
-- Remove the channel prefix from translation text to be clean SFrames.Chat:AppendAutoTranslatedLine(tabId, "channel", chanName, cleanText, result, senderName)
cleanText = string.gsub(cleanText, "^%[.-%]%s*", "") end
-- It might also have [PlayerName]: end)
local _, _, senderName = string.find(cleanText, "^%[([^%]]+)%]:%s*")
if senderName then
cleanText = string.gsub(cleanText, "^%[[^%]]+%]:%s*", "")
end
if cleanText ~= "" then
local tabId = tab.id
SFrames.Chat:RequestAutoTranslation(cleanText, function(result)
if result and result ~= "" then
SFrames.Chat:AppendAutoTranslatedLine(tabId, "channel", chanName, cleanText, result, senderName)
end
end)
end
end end
end end
else else
@@ -6655,7 +6734,6 @@ function SFrames.Chat:Initialize()
end end
end end
-- Hardcore Death Event Overrides
if db.hcDeathDisable or (db.hcDeathLevelMin and db.hcDeathLevelMin > 1) then if db.hcDeathDisable or (db.hcDeathLevelMin and db.hcDeathLevelMin > 1) then
local deathLvl = ParseHardcoreDeathMessage(text) local deathLvl = ParseHardcoreDeathMessage(text)
if deathLvl then if deathLvl then
@@ -6667,47 +6745,13 @@ function SFrames.Chat:Initialize()
SFrames.Chat.MessageIndex = SFrames.Chat.MessageIndex + 1 SFrames.Chat.MessageIndex = SFrames.Chat.MessageIndex + 1
local msgID = SFrames.Chat.MessageIndex local msgID = SFrames.Chat.MessageIndex
-- Store in runtime lookup
SFrames.Chat.MessageHistory[msgID] = text SFrames.Chat.MessageHistory[msgID] = text
-- Persist to SavedVariables PersistMessage(msgID, text, r, g, b, _cfTabCache[self])
local frameIdx = nil
for fi = 1, 7 do
if self == _G["ChatFrame" .. fi] then
frameIdx = fi
break
end
end
PersistMessage(msgID, text, r, g, b, frameIdx)
-- Apply class color to player names in message
local coloredText = ColorPlayerNamesInText(text) local coloredText = ColorPlayerNamesInText(text)
-- Insert the clickable button [+] at the beginning
local modifiedText = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. coloredText local modifiedText = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. coloredText
origAddMessage(self, modifiedText, r, g, b, alpha, holdTime) origAddMessage(self, modifiedText, r, g, b, alpha, holdTime)
-- Legacy auto-translate disabled; handled by per-tab routing above.
if false then
if self == ChatFrame1 and not string.find(text, "%[翻译%]") then
if string.find(text, "%[.-硬核.-%]") or string.find(string.lower(text), "%[.-hc.-%]") or string.find(string.lower(text), "%[.-hardcore.-%]") then
local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
cleanText = string.gsub(cleanText, "|r", "")
cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1")
if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then
_G.STranslateAPI.Translate(cleanText, "auto", "zh", function(result, err, meta)
if result then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[翻译] |cffffff00" .. tostring(result) .. "|r")
end
end, "Nanami-UI")
elseif _G.STranslate and _G.STranslate.SendIO then
_G.STranslate:SendIO(cleanText, "IN", "auto", "zh")
end
end
end
end
else
origAddMessage(self, text, r, g, b, alpha, holdTime)
end end
end end
end end

View File

@@ -509,7 +509,6 @@ local function EnsureDB()
if type(SFramesDB.Tweaks) ~= "table" then SFramesDB.Tweaks = {} end if type(SFramesDB.Tweaks) ~= "table" then SFramesDB.Tweaks = {} end
if SFramesDB.Tweaks.autoStance == nil then SFramesDB.Tweaks.autoStance = true 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.superWoW == nil then SFramesDB.Tweaks.superWoW = true end
if SFramesDB.Tweaks.turtleCompat == nil then SFramesDB.Tweaks.turtleCompat = true end if SFramesDB.Tweaks.turtleCompat == nil then SFramesDB.Tweaks.turtleCompat = true end
if SFramesDB.Tweaks.cooldownNumbers == nil then SFramesDB.Tweaks.cooldownNumbers = true end if SFramesDB.Tweaks.cooldownNumbers == nil then SFramesDB.Tweaks.cooldownNumbers = true end
@@ -1090,14 +1089,7 @@ function SFrames.ConfigUI:BuildUIPage()
CreateDesc(tweaksSection, "施法需要特定姿态时自动切换(如人形按熊掌→自动变熊)", 36, -50, font, 218) CreateDesc(tweaksSection, "施法需要特定姿态时自动切换(如人形按熊掌→自动变熊)", 36, -50, font, 218)
table.insert(controls, CreateCheckBox(tweaksSection, table.insert(controls, CreateCheckBox(tweaksSection,
"自动取消变形/下马", 270, -34, "乌龟服兼容修改", 270, -34,
function() return SFramesDB.Tweaks.autoDismount ~= false end,
function(checked) SFramesDB.Tweaks.autoDismount = checked end
))
CreateDesc(tweaksSection, "变形/骑马时施法自动取消(如熊形按治疗→自动回人形)", 292, -50, font, 218)
table.insert(controls, CreateCheckBox(tweaksSection,
"乌龟服兼容修改", 14, -80,
function() return SFramesDB.Tweaks.turtleCompat ~= false end, function() return SFramesDB.Tweaks.turtleCompat ~= false end,
function(checked) SFramesDB.Tweaks.turtleCompat = checked end function(checked) SFramesDB.Tweaks.turtleCompat = checked end
)) ))

View File

@@ -83,9 +83,11 @@ end
-- Event Dispatcher -- Event Dispatcher
SFrames.eventFrame:SetScript("OnEvent", function() SFrames.eventFrame:SetScript("OnEvent", function()
if SFrames.events[event] then local handlers = SFrames.events[event]
for i, func in ipairs(SFrames.events[event]) do if handlers then
func(event) local n = table.getn(handlers)
for i = 1, n do
handlers[i](event)
end end
end end
end) end)
@@ -100,8 +102,8 @@ end
function SFrames:UnregisterEvent(event, func) function SFrames:UnregisterEvent(event, func)
if self.events[event] then if self.events[event] then
for i, f in ipairs(self.events[event]) do for i = 1, table.getn(self.events[event]) do
if f == func then if self.events[event][i] == func then
table.remove(self.events[event], i) table.remove(self.events[event], i)
break break
end end

View File

@@ -147,7 +147,11 @@ function SFrames.FloatingTooltip:Initialize()
bg:SetAllPoints(bgFrame) bg:SetAllPoints(bgFrame)
GameTooltip._nanamiBGTex = bg GameTooltip._nanamiBGTex = bg
bgFrame._ttVisCheck = 0
bgFrame:SetScript("OnUpdate", function() 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 if not GameTooltip:IsVisible() then
this:Hide() this:Hide()
end end

View File

@@ -812,10 +812,20 @@ local function UpdateFilters()
MainFrame.filterUsed:SetActive(currentFilter == "used") MainFrame.filterUsed:SetActive(currentFilter == "used")
end end
local _isUpdating = false
local function FullUpdate() local function FullUpdate()
UpdateFilters() if _isUpdating then return end
UpdateList() _isUpdating = true
UpdateDetail() 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 end
local function SelectService(index) local function SelectService(index)
@@ -1181,12 +1191,6 @@ function TUI:Initialize()
ClassTrainerFrame:EnableMouse(false) ClassTrainerFrame:EnableMouse(false)
end end
if SetTrainerServiceTypeFilter then
SetTrainerServiceTypeFilter("available", 1)
SetTrainerServiceTypeFilter("unavailable", 1)
SetTrainerServiceTypeFilter("used", 1)
end
selectedIndex = nil selectedIndex = nil
currentFilter = "all" currentFilter = "all"
collapsedCats = {} collapsedCats = {}
@@ -1196,6 +1200,15 @@ function TUI:Initialize()
npcName = npcName .. " - 专业训练" npcName = npcName .. " - 专业训练"
end end
MainFrame.npcNameFS:SetText(npcName) 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() MainFrame:Show()
BuildDisplayList() BuildDisplayList()
for _, entry in ipairs(displayList) do for _, entry in ipairs(displayList) do

View File

@@ -6,8 +6,7 @@
-- 4. Cooldown Numbers - show remaining cooldown time as text overlay -- 4. Cooldown Numbers - show remaining cooldown time as text overlay
-- 5. Dark UI - darken the entire interface -- 5. Dark UI - darken the entire interface
-- 6. WorldMap Window - turn fullscreen map into a movable/scalable window -- 6. WorldMap Window - turn fullscreen map into a movable/scalable window
-- 7. Auto Dismount - cancel shapeshift/mount when casting incompatible spells -- 7. Hunter Aspect Guard - cancel Cheetah/Pack when taking damage in combat (avoid OOC false positives)
-- 8. Hunter Aspect Guard - auto switch to Hawk when taking damage with Cheetah/Pack
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
SFrames.Tweaks = SFrames.Tweaks or {} SFrames.Tweaks = SFrames.Tweaks or {}
@@ -75,120 +74,6 @@ local function InitAutoStance()
end) end)
end end
--------------------------------------------------------------------------------
-- Auto Dismount / Cancel Shapeshift
-- When casting a spell that fails because you are mounted or shapeshifted,
-- automatically cancel the mount/shapeshift buff so the next cast succeeds.
--------------------------------------------------------------------------------
local function InitAutoDismount()
local dismount = CreateFrame("Frame", "NanamiAutoDismount")
local _, 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 -- SuperWoW Compatibility
-- Provides GUID-based cast/channel data when SuperWoW client mod is active. -- Provides GUID-based cast/channel data when SuperWoW client mod is active.
@@ -389,6 +274,78 @@ local function TimeConvert(remaining)
end end
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 function CooldownOnUpdate()
local parent = this:GetParent() local parent = this:GetParent()
if not parent then this:Hide() return end 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) cooldown.cooldowntext.text:SetPoint("CENTER", cooldown.cooldowntext, "CENTER", 0, 0)
end end
cooldown.cooldowntext:SetScript("OnUpdate", CooldownOnUpdate) RegisterCooldownFrame(cooldown.cooldowntext)
end end
local function SetCooldown(frame, start, duration, enable) 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 start > 0 and duration > 0 and (not enable or enable > 0) then
if frame.cooldownmask then frame.cooldownmask:Show() end if frame.cooldownmask then frame.cooldownmask:Show() end
frame.cooldowntext:Show() frame.cooldowntext:Show()
frame.cooldowntext.start = start
frame.cooldowntext.duration = duration
RegisterCooldownFrame(frame.cooldowntext)
else else
if frame.cooldownmask then frame.cooldownmask:Hide() end if frame.cooldownmask then frame.cooldownmask:Hide() end
frame.cooldowntext:Hide() frame.cooldowntext:Hide()
end end
frame.cooldowntext.start = start
frame.cooldowntext.duration = duration
end end
end end
@@ -1061,8 +1019,8 @@ end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Hunter Aspect Guard -- Hunter Aspect Guard
-- When a Hunter takes damage with Aspect of the Cheetah or Aspect of the Pack -- When a Hunter takes damage in combat with Aspect of the Cheetah or Pack
-- active, automatically cancel the aspect to prevent repeated dazing. -- active, cancel the aspect to reduce daze chains. OOC HP changes are ignored.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
local function InitHunterAspectGuard() local function InitHunterAspectGuard()
local _, playerClass = UnitClass("player") local _, playerClass = UnitClass("player")
@@ -1104,7 +1062,7 @@ local function InitHunterAspectGuard()
return return
end 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 GetTime() - lastCancel >= 1.0 then
if CancelDangerousAspect() then if CancelDangerousAspect() then
lastCancel = GetTime() lastCancel = GetTime()
@@ -1129,13 +1087,6 @@ function Tweaks:Initialize()
end end
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 if cfg.superWoW ~= false then
local ok, err = pcall(InitSuperWoW) local ok, err = pcall(InitSuperWoW)
if not ok then if not ok then

View File

@@ -493,7 +493,7 @@ function SFrames.Party:Initialize()
if not self._globalUpdateFrame then if not self._globalUpdateFrame then
self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent) self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent)
self._globalUpdateFrame:SetScript("OnUpdate", function() self._globalUpdateFrame._onUpdateFunc = function()
if SFrames.Party.testing then return end if SFrames.Party.testing then return end
local dt = arg1 local dt = arg1
local frames = SFrames.Party.frames local frames = SFrames.Party.frames
@@ -533,7 +533,8 @@ function SFrames.Party:Initialize()
end end
end end
end end
end) end
self._globalUpdateFrame:Hide()
end end
self:ApplyConfig() self:ApplyConfig()
@@ -745,20 +746,35 @@ function SFrames.Party:UpdateAll()
self.frames[i].frame:Hide() self.frames[i].frame:Hide()
end end
end end
if self._globalUpdateFrame then
self._globalUpdateFrame:SetScript("OnUpdate", nil)
self._globalUpdateFrame:Hide()
end
return return
end end
if self.testing then return end if self.testing then return end
local numParty = GetNumPartyMembers() local numParty = GetNumPartyMembers()
local hasVisible = false
for i = 1, 4 do for i = 1, 4 do
local data = self.frames[i] local data = self.frames[i]
local f = data.frame local f = data.frame
if i <= numParty then if i <= numParty then
f:Show() f:Show()
self:UpdateFrame(data.unit) self:UpdateFrame(data.unit)
hasVisible = true
else else
f:Hide() f:Hide()
end end
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 end
function SFrames.Party:UpdateFrame(unit) function SFrames.Party:UpdateFrame(unit)

View File

@@ -539,11 +539,13 @@ function SFrames.Raid:EnsureFrames()
if not self._globalUpdateFrame then if not self._globalUpdateFrame then
self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent) self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent)
self._globalUpdateFrame:SetScript("OnUpdate", function() self._globalUpdateFrame._onUpdateFunc = function()
if SFrames.Raid.testing then return end if SFrames.Raid.testing then return end
local dt = arg1 local dt = arg1
local frames = SFrames.Raid.frames local frames = SFrames.Raid.frames
if not frames then return end if not frames then return end
local visibleCount = SFrames.Raid._visibleCount or 0
if visibleCount == 0 then return end
for i = 1, 40 do for i = 1, 40 do
local entry = frames[i] local entry = frames[i]
if entry then if entry then
@@ -573,7 +575,8 @@ function SFrames.Raid:EnsureFrames()
end end
end end
end end
end) end
self._globalUpdateFrame:Hide()
end end
self:ApplyLayout() self:ApplyLayout()
@@ -602,6 +605,7 @@ function SFrames.Raid:UpdateAll()
groupSlots[g] = 0 groupSlots[g] = 0
end end
local visibleCount = 0
for i = 1, 40 do for i = 1, 40 do
local name, rank, subgroup = GetRaidRosterInfo(i) local name, rank, subgroup = GetRaidRosterInfo(i)
if name and subgroup and subgroup >= 1 and subgroup <= 8 then 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 self.frames[frameIndex].unit = "raid" .. i
f:Show() f:Show()
self:UpdateFrame("raid" .. i) self:UpdateFrame("raid" .. i)
visibleCount = visibleCount + 1
end end
end 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 -- Show/hide group labels based on whether each group has members
local showLabel = SFramesDB and SFramesDB.raidShowGroupLabel ~= false local showLabel = SFramesDB and SFramesDB.raidShowGroupLabel ~= false
@@ -632,6 +643,11 @@ function SFrames.Raid:UpdateAll()
end end
end end
else 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 if not self.testing and self._framesBuilt then
for i = 1, 40 do for i = 1, 40 do
self.frames[i].frame:Hide() self.frames[i].frame:Hide()

View 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`